@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.
package/dist/cli.js ADDED
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, statSync, readFileSync, readdirSync } from 'fs';
3
+ import { resolve, join, relative } from 'path';
4
+ import process from 'process';
5
+ import { z } from 'zod';
6
+
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
+
50
+ // src/scanner.ts
51
+ var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
52
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
53
+ "node_modules",
54
+ "dist",
55
+ ".next",
56
+ ".open-next",
57
+ ".turbo",
58
+ "build",
59
+ "out",
60
+ "coverage",
61
+ ".git"
62
+ ]);
63
+ var PATTERNS = {
64
+ /** ESM/CJS import OR a `require()` of @openkeyai/ui. */
65
+ uiImport: /\bfrom\s+['"]@openkeyai\/ui(?:\/[^'"]+)?['"]|require\(\s*['"]@openkeyai\/ui(?:\/[^'"]+)?['"]/,
66
+ /** Mounted JSX: `<HubHeader …` (open tag). */
67
+ hubHeaderJsx: /<\s*HubHeader\b/,
68
+ /** Named import containing `HubHeader`. */
69
+ hubHeaderImport: /\bimport\b[^;\n]*?\bHubHeader\b[^;\n]*?from\s+['"]@openkeyai\/ui/,
70
+ /** Any import from `@openkeyai/sdk/_internal/*` — banned. */
71
+ bannedInternal: /from\s+['"]@openkeyai\/sdk\/_internal(?:\/[^'"]*)?['"]|require\(\s*['"]@openkeyai\/sdk\/_internal(?:\/[^'"]*)?['"]/
72
+ };
73
+ function isLayoutFile(relPath) {
74
+ const segments = relPath.split(/[\\/]/);
75
+ const base = segments[segments.length - 1] ?? "";
76
+ return /^layout\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
77
+ }
78
+ function* walkSourceFiles(root, dir) {
79
+ let entries;
80
+ try {
81
+ entries = readdirSync(dir, { withFileTypes: true });
82
+ } catch {
83
+ return;
84
+ }
85
+ for (const entry of entries) {
86
+ if (entry.isDirectory()) {
87
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
88
+ yield* walkSourceFiles(root, join(dir, entry.name));
89
+ continue;
90
+ }
91
+ if (!entry.isFile()) continue;
92
+ const idx = entry.name.lastIndexOf(".");
93
+ const ext = idx === -1 ? "" : entry.name.slice(idx).toLowerCase();
94
+ if (!SOURCE_EXTS.has(ext)) continue;
95
+ const absPath = join(dir, entry.name);
96
+ yield { relPath: relative(root, absPath), absPath };
97
+ }
98
+ }
99
+ function lineOf(text, regex) {
100
+ const m = regex.exec(text);
101
+ if (!m) return void 0;
102
+ const upto = text.slice(0, m.index);
103
+ return upto.split("\n").length;
104
+ }
105
+ function scanToolSource(input) {
106
+ const violations = [];
107
+ let scannedFileCount = 0;
108
+ let foundHubHeaderImport = false;
109
+ let foundHubHeaderMount = false;
110
+ let firstLayoutFile;
111
+ let srcExists = false;
112
+ try {
113
+ statSync(input.srcDir);
114
+ srcExists = true;
115
+ } catch {
116
+ violations.push({
117
+ code: "io-error",
118
+ message: `srcDir does not exist: ${input.srcDir}`
119
+ });
120
+ }
121
+ if (srcExists) {
122
+ for (const { relPath, absPath } of walkSourceFiles(
123
+ input.srcDir,
124
+ input.srcDir
125
+ )) {
126
+ scannedFileCount += 1;
127
+ let body;
128
+ try {
129
+ body = readFileSync(absPath, "utf8");
130
+ } catch (err) {
131
+ violations.push({
132
+ code: "io-error",
133
+ message: `Could not read file: ${err instanceof Error ? err.message : "unknown"}`,
134
+ file: relPath
135
+ });
136
+ continue;
137
+ }
138
+ const bannedLine = lineOf(body, PATTERNS.bannedInternal);
139
+ if (bannedLine !== void 0) {
140
+ violations.push({
141
+ code: "banned-internal-import",
142
+ message: "@openkeyai/sdk/_internal is not a public surface and may change without notice. Import only from the package root.",
143
+ file: relPath,
144
+ line: bannedLine
145
+ });
146
+ }
147
+ if (isLayoutFile(relPath)) {
148
+ if (firstLayoutFile === void 0) firstLayoutFile = relPath;
149
+ if (PATTERNS.hubHeaderImport.test(body)) {
150
+ foundHubHeaderImport = true;
151
+ }
152
+ if (PATTERNS.hubHeaderJsx.test(body)) {
153
+ foundHubHeaderMount = true;
154
+ }
155
+ } else if (PATTERNS.uiImport.test(body) && PATTERNS.hubHeaderJsx.test(body)) {
156
+ foundHubHeaderMount = true;
157
+ if (PATTERNS.hubHeaderImport.test(body)) {
158
+ foundHubHeaderImport = true;
159
+ }
160
+ }
161
+ }
162
+ if (!foundHubHeaderImport) {
163
+ violations.push({
164
+ code: "missing-hubheader-import",
165
+ message: "No layout file imports `HubHeader` from `@openkeyai/ui`. Every tool must mount the shared header.",
166
+ file: firstLayoutFile ?? "app/layout.tsx"
167
+ });
168
+ }
169
+ if (!foundHubHeaderMount) {
170
+ violations.push({
171
+ code: "missing-hubheader-mount",
172
+ message: "`<HubHeader />` is not mounted anywhere in the source tree. Place it in your root layout.",
173
+ file: firstLayoutFile ?? "app/layout.tsx"
174
+ });
175
+ }
176
+ }
177
+ if (input.manifestPath) {
178
+ try {
179
+ const raw = readFileSync(input.manifestPath, "utf8");
180
+ const json = JSON.parse(raw);
181
+ const parsed = ToolManifestSchema.safeParse(json);
182
+ if (!parsed.success) {
183
+ for (const issue of parsed.error.issues) {
184
+ violations.push({
185
+ code: "manifest-invalid",
186
+ message: `${issue.path.join(".") || "(root)"} \u2014 ${issue.message}`,
187
+ file: input.manifestPath
188
+ });
189
+ }
190
+ }
191
+ } catch (err) {
192
+ violations.push({
193
+ code: "manifest-missing",
194
+ message: `Could not read tool.json: ${err instanceof Error ? err.message : "unknown"}`,
195
+ file: input.manifestPath
196
+ });
197
+ }
198
+ }
199
+ return { violations, scannedFileCount };
200
+ }
201
+
202
+ // src/cli.ts
203
+ function parseArgs(argv) {
204
+ let srcDir = null;
205
+ let manifestPath = void 0;
206
+ for (let i = 0; i < argv.length; i++) {
207
+ const a = argv[i];
208
+ if (a === "--src" || a === "-s") {
209
+ const v = argv[++i];
210
+ if (!v) usage("missing value for --src");
211
+ srcDir = v;
212
+ } else if (a === "--manifest" || a === "-m") {
213
+ const v = argv[++i];
214
+ if (!v) usage("missing value for --manifest");
215
+ manifestPath = v;
216
+ } else if (a === "--no-manifest") {
217
+ manifestPath = null;
218
+ } else if (a === "--help" || a === "-h") {
219
+ printHelp();
220
+ process.exit(0);
221
+ } else if (a !== void 0) {
222
+ usage(`unknown argument: ${a}`);
223
+ }
224
+ }
225
+ const resolvedSrc = srcDir ?? defaultSrcDir();
226
+ const resolvedManifest = manifestPath === null ? null : manifestPath ?? (existsSync("tool.json") ? "tool.json" : null);
227
+ return { srcDir: resolve(resolvedSrc), manifestPath: resolvedManifest };
228
+ }
229
+ function defaultSrcDir() {
230
+ if (existsSync("src")) return "src";
231
+ if (existsSync("app")) return "app";
232
+ return ".";
233
+ }
234
+ function usage(message) {
235
+ process.stderr.write(`okai-scan: ${message}
236
+ `);
237
+ printHelp(process.stderr);
238
+ process.exit(2);
239
+ }
240
+ function printHelp(stream = process.stdout) {
241
+ stream.write(
242
+ [
243
+ "Usage: okai-scan [options]",
244
+ "",
245
+ "Options:",
246
+ " -s, --src <dir> Source directory to scan (default: src/, then app/, then .)",
247
+ " -m, --manifest <path> Path to tool.json (default: ./tool.json if present)",
248
+ " --no-manifest Skip the manifest check entirely",
249
+ " -h, --help Show this help",
250
+ "",
251
+ "Exit codes:",
252
+ " 0 no violations",
253
+ " 1 one or more violations",
254
+ " 2 bad CLI usage",
255
+ ""
256
+ ].join("\n")
257
+ );
258
+ }
259
+ function main() {
260
+ const args = parseArgs(process.argv.slice(2));
261
+ const result = scanToolSource({
262
+ srcDir: args.srcDir,
263
+ manifestPath: args.manifestPath ?? void 0
264
+ });
265
+ if (result.violations.length === 0) {
266
+ process.stdout.write(
267
+ `okai-scan: clean (${result.scannedFileCount} source files scanned)
268
+ `
269
+ );
270
+ process.exit(0);
271
+ }
272
+ process.stderr.write(
273
+ `okai-scan: ${result.violations.length} violation(s) (${result.scannedFileCount} source files scanned)
274
+
275
+ `
276
+ );
277
+ for (const v of result.violations) {
278
+ const where = v.file ? `${v.file}${v.line !== void 0 ? `:${v.line}` : ""}` : "(no file)";
279
+ process.stderr.write(` [${v.code}] ${where}
280
+ ${v.message}
281
+
282
+ `);
283
+ }
284
+ process.exit(1);
285
+ }
286
+ main();
287
+ //# sourceMappingURL=cli.js.map
288
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts","../src/scanner.ts","../src/cli.ts"],"names":[],"mappings":"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","file":"cli.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 { 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","#!/usr/bin/env node\n/**\n * `okai-scan` — CLI wrapper around `scanToolSource`.\n *\n * Usage (in a tool repo):\n *\n * npx okai-scan # scans ./src (or ./app) and ./tool.json\n * npx okai-scan --src app # explicit srcDir\n * npx okai-scan --no-manifest\n *\n * Exit codes:\n * 0 no violations\n * 1 one or more violations\n * 2 bad CLI usage (unknown flag, missing arg)\n *\n * The intended primary consumer is the tool CI workflow shipped by the\n * `Scott-Builds-AI/.github` reusable workflows (Phase 11). Tool authors can\n * also run it locally before pushing.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { scanToolSource } from \"./scanner\";\n\ntype Argv = {\n srcDir: string;\n manifestPath: string | null;\n};\n\nfunction parseArgs(argv: string[]): Argv {\n let srcDir: string | null = null;\n let manifestPath: string | null | undefined = undefined; // undefined = use default\n\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n if (a === \"--src\" || a === \"-s\") {\n const v = argv[++i];\n if (!v) usage(\"missing value for --src\");\n srcDir = v;\n } else if (a === \"--manifest\" || a === \"-m\") {\n const v = argv[++i];\n if (!v) usage(\"missing value for --manifest\");\n manifestPath = v;\n } else if (a === \"--no-manifest\") {\n manifestPath = null;\n } else if (a === \"--help\" || a === \"-h\") {\n printHelp();\n process.exit(0);\n } else if (a !== undefined) {\n usage(`unknown argument: ${a}`);\n }\n }\n\n // Resolve defaults.\n const resolvedSrc = srcDir ?? defaultSrcDir();\n const resolvedManifest =\n manifestPath === null\n ? null\n : (manifestPath ?? (existsSync(\"tool.json\") ? \"tool.json\" : null));\n\n return { srcDir: resolve(resolvedSrc), manifestPath: resolvedManifest };\n}\n\nfunction defaultSrcDir(): string {\n if (existsSync(\"src\")) return \"src\";\n if (existsSync(\"app\")) return \"app\";\n return \".\";\n}\n\nfunction usage(message: string): never {\n process.stderr.write(`okai-scan: ${message}\\n`);\n printHelp(process.stderr);\n process.exit(2);\n}\n\nfunction printHelp(stream: NodeJS.WriteStream = process.stdout): void {\n stream.write(\n [\n \"Usage: okai-scan [options]\",\n \"\",\n \"Options:\",\n \" -s, --src <dir> Source directory to scan (default: src/, then app/, then .)\",\n \" -m, --manifest <path> Path to tool.json (default: ./tool.json if present)\",\n \" --no-manifest Skip the manifest check entirely\",\n \" -h, --help Show this help\",\n \"\",\n \"Exit codes:\",\n \" 0 no violations\",\n \" 1 one or more violations\",\n \" 2 bad CLI usage\",\n \"\",\n ].join(\"\\n\"),\n );\n}\n\nfunction main(): void {\n const args = parseArgs(process.argv.slice(2));\n const result = scanToolSource({\n srcDir: args.srcDir,\n manifestPath: args.manifestPath ?? undefined,\n });\n\n if (result.violations.length === 0) {\n process.stdout.write(\n `okai-scan: clean (${result.scannedFileCount} source files scanned)\\n`,\n );\n process.exit(0);\n }\n\n process.stderr.write(\n `okai-scan: ${result.violations.length} violation(s) ` +\n `(${result.scannedFileCount} source files scanned)\\n\\n`,\n );\n for (const v of result.violations) {\n const where = v.file\n ? `${v.file}${v.line !== undefined ? `:${v.line}` : \"\"}`\n : \"(no file)\";\n process.stderr.write(` [${v.code}] ${where}\\n ${v.message}\\n\\n`);\n }\n process.exit(1);\n}\n\nmain();\n"]}
package/dist/index.cjs ADDED
@@ -0,0 +1,231 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+
7
+ // src/schema.ts
8
+ var slugRegex = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
9
+ var ToolScopeEnum = zod.z.enum(["keys.read", "user.read", "billing.read"]);
10
+ var ProviderSlugEnum = zod.z.enum([
11
+ "openai",
12
+ "anthropic",
13
+ "google",
14
+ "replicate",
15
+ "elevenlabs",
16
+ "fal"
17
+ ]);
18
+ var RuntimeTierEnum = zod.z.enum(["edge", "container"]);
19
+ var ToolOwnerSchema = zod.z.object({
20
+ name: zod.z.string().min(1).max(80),
21
+ email: zod.z.string().email().optional(),
22
+ github: zod.z.string().regex(/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}[a-zA-Z0-9])?$/, "github handle").optional()
23
+ });
24
+ var ToolManifestSchema = zod.z.object({
25
+ /** Stable URL-safe identifier. Becomes the JWT's `aud` claim. */
26
+ slug: zod.z.string().regex(slugRegex, "slug must be lowercase kebab-case, 3\u201340 chars"),
27
+ /** Human-readable name shown in the catalog and HubHeader. */
28
+ name: zod.z.string().min(1).max(80),
29
+ /** One-sentence catalog description. */
30
+ description: zod.z.string().max(280).default(""),
31
+ /** Tool version (semver). */
32
+ version: zod.z.string().regex(/^\d+\.\d+\.\d+(?:-[\w.+-]+)?$/, "semver"),
33
+ /** Runtime tier — picks template + deploy pipeline. */
34
+ runtime: RuntimeTierEnum,
35
+ /** Scopes the tool may request when minting a JWT. Min 1. */
36
+ scopes: zod.z.array(ToolScopeEnum).min(1),
37
+ /** API provider slugs the tool may fetch keys for. */
38
+ providers: zod.z.array(ProviderSlugEnum).default([]),
39
+ /** Public homepage / docs URL. Surfaced in the hub catalog. */
40
+ homepage: zod.z.string().url().optional(),
41
+ /** Where the hub sends users after issuing a token. https only. */
42
+ callback_url: zod.z.string().url().refine((u) => u.startsWith("https://"), "callback_url must be https://"),
43
+ /** Tool owner. */
44
+ owner: ToolOwnerSchema,
45
+ /** Semver range the tool was built against. Hub uses this for compat warnings. */
46
+ sdk_version: zod.z.string().min(1),
47
+ /** Optional category for catalog grouping (e.g. "media", "writing"). */
48
+ category: zod.z.string().min(1).max(40).optional()
49
+ });
50
+ function validateManifest(input) {
51
+ let json;
52
+ if (typeof input === "string") {
53
+ json = JSON.parse(input);
54
+ } else {
55
+ json = input;
56
+ }
57
+ return ToolManifestSchema.parse(json);
58
+ }
59
+ function validateManifestFile(path) {
60
+ const raw = fs.readFileSync(path, "utf8");
61
+ return validateManifest(raw);
62
+ }
63
+ var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
64
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
65
+ "node_modules",
66
+ "dist",
67
+ ".next",
68
+ ".open-next",
69
+ ".turbo",
70
+ "build",
71
+ "out",
72
+ "coverage",
73
+ ".git"
74
+ ]);
75
+ var PATTERNS = {
76
+ /** ESM/CJS import OR a `require()` of @openkeyai/ui. */
77
+ uiImport: /\bfrom\s+['"]@openkeyai\/ui(?:\/[^'"]+)?['"]|require\(\s*['"]@openkeyai\/ui(?:\/[^'"]+)?['"]/,
78
+ /** Mounted JSX: `<HubHeader …` (open tag). */
79
+ hubHeaderJsx: /<\s*HubHeader\b/,
80
+ /** Named import containing `HubHeader`. */
81
+ hubHeaderImport: /\bimport\b[^;\n]*?\bHubHeader\b[^;\n]*?from\s+['"]@openkeyai\/ui/,
82
+ /** Any import from `@openkeyai/sdk/_internal/*` — banned. */
83
+ bannedInternal: /from\s+['"]@openkeyai\/sdk\/_internal(?:\/[^'"]*)?['"]|require\(\s*['"]@openkeyai\/sdk\/_internal(?:\/[^'"]*)?['"]/
84
+ };
85
+ function isLayoutFile(relPath) {
86
+ const segments = relPath.split(/[\\/]/);
87
+ const base = segments[segments.length - 1] ?? "";
88
+ return /^layout\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
89
+ }
90
+ function* walkSourceFiles(root, dir) {
91
+ let entries;
92
+ try {
93
+ entries = fs.readdirSync(dir, { withFileTypes: true });
94
+ } catch {
95
+ return;
96
+ }
97
+ for (const entry of entries) {
98
+ if (entry.isDirectory()) {
99
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
100
+ yield* walkSourceFiles(root, path.join(dir, entry.name));
101
+ continue;
102
+ }
103
+ if (!entry.isFile()) continue;
104
+ const idx = entry.name.lastIndexOf(".");
105
+ const ext = idx === -1 ? "" : entry.name.slice(idx).toLowerCase();
106
+ if (!SOURCE_EXTS.has(ext)) continue;
107
+ const absPath = path.join(dir, entry.name);
108
+ yield { relPath: path.relative(root, absPath), absPath };
109
+ }
110
+ }
111
+ function lineOf(text, regex) {
112
+ const m = regex.exec(text);
113
+ if (!m) return void 0;
114
+ const upto = text.slice(0, m.index);
115
+ return upto.split("\n").length;
116
+ }
117
+ function scanToolSource(input) {
118
+ const violations = [];
119
+ let scannedFileCount = 0;
120
+ let foundHubHeaderImport = false;
121
+ let foundHubHeaderMount = false;
122
+ let firstLayoutFile;
123
+ let srcExists = false;
124
+ try {
125
+ fs.statSync(input.srcDir);
126
+ srcExists = true;
127
+ } catch {
128
+ violations.push({
129
+ code: "io-error",
130
+ message: `srcDir does not exist: ${input.srcDir}`
131
+ });
132
+ }
133
+ if (srcExists) {
134
+ for (const { relPath, absPath } of walkSourceFiles(
135
+ input.srcDir,
136
+ input.srcDir
137
+ )) {
138
+ scannedFileCount += 1;
139
+ let body;
140
+ try {
141
+ body = fs.readFileSync(absPath, "utf8");
142
+ } catch (err) {
143
+ violations.push({
144
+ code: "io-error",
145
+ message: `Could not read file: ${err instanceof Error ? err.message : "unknown"}`,
146
+ file: relPath
147
+ });
148
+ continue;
149
+ }
150
+ const bannedLine = lineOf(body, PATTERNS.bannedInternal);
151
+ if (bannedLine !== void 0) {
152
+ violations.push({
153
+ code: "banned-internal-import",
154
+ message: "@openkeyai/sdk/_internal is not a public surface and may change without notice. Import only from the package root.",
155
+ file: relPath,
156
+ line: bannedLine
157
+ });
158
+ }
159
+ if (isLayoutFile(relPath)) {
160
+ if (firstLayoutFile === void 0) firstLayoutFile = relPath;
161
+ if (PATTERNS.hubHeaderImport.test(body)) {
162
+ foundHubHeaderImport = true;
163
+ }
164
+ if (PATTERNS.hubHeaderJsx.test(body)) {
165
+ foundHubHeaderMount = true;
166
+ }
167
+ } else if (PATTERNS.uiImport.test(body) && PATTERNS.hubHeaderJsx.test(body)) {
168
+ foundHubHeaderMount = true;
169
+ if (PATTERNS.hubHeaderImport.test(body)) {
170
+ foundHubHeaderImport = true;
171
+ }
172
+ }
173
+ }
174
+ if (!foundHubHeaderImport) {
175
+ violations.push({
176
+ code: "missing-hubheader-import",
177
+ message: "No layout file imports `HubHeader` from `@openkeyai/ui`. Every tool must mount the shared header.",
178
+ file: firstLayoutFile ?? "app/layout.tsx"
179
+ });
180
+ }
181
+ if (!foundHubHeaderMount) {
182
+ violations.push({
183
+ code: "missing-hubheader-mount",
184
+ message: "`<HubHeader />` is not mounted anywhere in the source tree. Place it in your root layout.",
185
+ file: firstLayoutFile ?? "app/layout.tsx"
186
+ });
187
+ }
188
+ }
189
+ if (input.manifestPath) {
190
+ try {
191
+ const raw = fs.readFileSync(input.manifestPath, "utf8");
192
+ const json = JSON.parse(raw);
193
+ const parsed = ToolManifestSchema.safeParse(json);
194
+ if (!parsed.success) {
195
+ for (const issue of parsed.error.issues) {
196
+ violations.push({
197
+ code: "manifest-invalid",
198
+ message: `${issue.path.join(".") || "(root)"} \u2014 ${issue.message}`,
199
+ file: input.manifestPath
200
+ });
201
+ }
202
+ }
203
+ } catch (err) {
204
+ violations.push({
205
+ code: "manifest-missing",
206
+ message: `Could not read tool.json: ${err instanceof Error ? err.message : "unknown"}`,
207
+ file: input.manifestPath
208
+ });
209
+ }
210
+ }
211
+ return { violations, scannedFileCount };
212
+ }
213
+
214
+ // src/index.ts
215
+ var TOOL_MANIFEST_VERSION = "0.1.0";
216
+
217
+ Object.defineProperty(exports, "ZodError", {
218
+ enumerable: true,
219
+ get: function () { return zod.ZodError; }
220
+ });
221
+ exports.ProviderSlugEnum = ProviderSlugEnum;
222
+ exports.RuntimeTierEnum = RuntimeTierEnum;
223
+ exports.TOOL_MANIFEST_VERSION = TOOL_MANIFEST_VERSION;
224
+ exports.ToolManifestSchema = ToolManifestSchema;
225
+ exports.ToolOwnerSchema = ToolOwnerSchema;
226
+ exports.ToolScopeEnum = ToolScopeEnum;
227
+ exports.scanToolSource = scanToolSource;
228
+ exports.validateManifest = validateManifest;
229
+ exports.validateManifestFile = validateManifestFile;
230
+ //# sourceMappingURL=index.cjs.map
231
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts","../src/validate.ts","../src/scanner.ts","../src/index.ts"],"names":["z","readFileSync","readdirSync","join","relative","statSync"],"mappings":"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","file":"index.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","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"]}