@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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/cli.cjs +294 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +288 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +231 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +218 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.cjs +59 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +69 -0
- package/dist/schema.d.ts +69 -0
- package/dist/schema.js +50 -0
- package/dist/schema.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scott Goodwin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# `@openkeyai/tool-manifest`
|
|
2
|
+
|
|
3
|
+
Schema + AST scanner that defines and enforces the [OpenKey AI tool contract](https://github.com/Scott-Builds-AI/hub/blob/main/docs/TOOL_CONTRACT.md). Used by tool CI and the Phase 13 auto-scaffolder.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add -D @openkeyai/tool-manifest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What this package gives you
|
|
10
|
+
|
|
11
|
+
| Export | Purpose |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `ToolManifestSchema` (zod) | The FROZEN shape of every tool's `tool.json` |
|
|
14
|
+
| `validateManifest(json)` | Parse + validate. Returns typed `ToolManifest` or throws `ZodError` |
|
|
15
|
+
| `validateManifestFile(path)` | Same, reading from disk |
|
|
16
|
+
| `scanToolSource({ srcDir, manifestPath? })` | Walks the tool's source tree, returns contract violations |
|
|
17
|
+
| `okai-scan` CLI | Thin wrapper around `scanToolSource` for `npx okai-scan` |
|
|
18
|
+
|
|
19
|
+
## `tool.json` — the manifest
|
|
20
|
+
|
|
21
|
+
Every tool ships a `tool.json` at its repo root:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"slug": "yt-thumbnails",
|
|
26
|
+
"name": "YouTube Thumbnails",
|
|
27
|
+
"description": "AI-generated thumbnails for YouTube videos.",
|
|
28
|
+
"version": "0.1.0",
|
|
29
|
+
"runtime": "edge",
|
|
30
|
+
"scopes": ["keys.read"],
|
|
31
|
+
"providers": ["openai"],
|
|
32
|
+
"homepage": "https://yt-thumbnails.openkeyai.com",
|
|
33
|
+
"callback_url": "https://yt-thumbnails.openkeyai.com/start",
|
|
34
|
+
"owner": {
|
|
35
|
+
"name": "Scott Goodwin",
|
|
36
|
+
"email": "scott@example.com",
|
|
37
|
+
"github": "scottgoodwin"
|
|
38
|
+
},
|
|
39
|
+
"sdk_version": "^0.1.0"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Field | Required | Notes |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `slug` | ✓ | Lowercase kebab-case, 3–40 chars. Becomes the JWT `aud` claim. |
|
|
46
|
+
| `name` | ✓ | Display name. |
|
|
47
|
+
| `description` | | One-sentence catalog blurb. |
|
|
48
|
+
| `version` | ✓ | semver (`x.y.z`). |
|
|
49
|
+
| `runtime` | ✓ | `edge` (Cloudflare Workers) or `container` (Fly Machines). |
|
|
50
|
+
| `scopes` | ✓ | Subset of `keys.read`, `user.read`, `billing.read`. Min 1. |
|
|
51
|
+
| `providers` | | Provider slugs the tool may request keys for. |
|
|
52
|
+
| `callback_url` | ✓ | Where the hub sends users after issuing a token. **https only.** |
|
|
53
|
+
| `owner` | ✓ | At least a name; email + github are nice-to-have for support. |
|
|
54
|
+
| `sdk_version` | ✓ | semver range — the SDK version the tool was built against. |
|
|
55
|
+
| `category`, `homepage` | | Optional catalog metadata. |
|
|
56
|
+
|
|
57
|
+
Schema lives in [`src/schema.ts`](src/schema.ts).
|
|
58
|
+
|
|
59
|
+
## Scanner — what it catches
|
|
60
|
+
|
|
61
|
+
Run from your tool repo:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx okai-scan
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or programmatically:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { scanToolSource } from "@openkeyai/tool-manifest";
|
|
71
|
+
|
|
72
|
+
const { violations, scannedFileCount } = scanToolSource({
|
|
73
|
+
srcDir: "src",
|
|
74
|
+
manifestPath: "tool.json",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (violations.length > 0) {
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Code | Catches |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `missing-hubheader-import` | No layout file imports `HubHeader` from `@openkeyai/ui` |
|
|
85
|
+
| `missing-hubheader-mount` | `HubHeader` imported but `<HubHeader />` never appears as JSX |
|
|
86
|
+
| `banned-internal-import` | Any source file imports from `@openkeyai/sdk/_internal/*` |
|
|
87
|
+
| `manifest-missing` / `manifest-invalid` | `tool.json` is unreadable or fails the Zod schema |
|
|
88
|
+
| `io-error` | Source dir is unreachable / unreadable |
|
|
89
|
+
|
|
90
|
+
The scanner uses regex-based text scanning rather than a full TypeScript AST. That keeps the dep tree small and CI fast. It catches the obvious violations; subtler patterns (like a HubHeader mount inside an unreachable branch) are out of scope for v1. If we ever need stricter checks the function signature stays the same — we'd swap the backend.
|
|
91
|
+
|
|
92
|
+
### Heuristics
|
|
93
|
+
|
|
94
|
+
- "Layout file" = any source file whose basename matches `layout.{ts,tsx,js,jsx,mjs,cjs}`. Covers Next.js App Router (`app/layout.tsx`) and the variants containers use.
|
|
95
|
+
- `node_modules`, `dist`, `.next`, `.open-next`, `.turbo`, `build`, `out`, `coverage`, and dot-directories are skipped during the walk.
|
|
96
|
+
|
|
97
|
+
## CLI
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
okai-scan [options]
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
-s, --src <dir> Source directory (default: src/, then app/, then .)
|
|
104
|
+
-m, --manifest <path> Path to tool.json (default: ./tool.json if present)
|
|
105
|
+
--no-manifest Skip the manifest check
|
|
106
|
+
-h, --help Show this help
|
|
107
|
+
|
|
108
|
+
Exit codes:
|
|
109
|
+
0 no violations
|
|
110
|
+
1 one or more violations
|
|
111
|
+
2 bad CLI usage
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Where this fits
|
|
115
|
+
|
|
116
|
+
- **Tool CI** ([Phase 11](https://github.com/Scott-Builds-AI/hub/blob/main/BUILD_PLAN.md)) runs `okai-scan` on every PR. Failed scans block merge.
|
|
117
|
+
- **Auto-scaffolder** ([Phase 13](https://github.com/Scott-Builds-AI/hub/blob/main/BUILD_PLAN.md)) uses `ToolManifestSchema` to generate a starter `tool.json` for newly-voted tools.
|
|
118
|
+
- **Hub tool registry** validates incoming manifest data against the same schema before inserting into `public.tools`.
|
|
119
|
+
|
|
120
|
+
## Versioning
|
|
121
|
+
|
|
122
|
+
- Semver. The schema's required-field set is frozen — additions are minor bumps, renames/removals are major (with 60 days notice per hub CLAUDE.md).
|
|
123
|
+
- The scanner rule set can grow without breaking consumers (a new rule reports a new code; existing codes don't change).
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pnpm install
|
|
129
|
+
pnpm typecheck
|
|
130
|
+
pnpm test # 17 tests across schema + scanner
|
|
131
|
+
pnpm build
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var process = require('process');
|
|
7
|
+
var zod = require('zod');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var process__default = /*#__PURE__*/_interopDefault(process);
|
|
12
|
+
|
|
13
|
+
var slugRegex = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
|
|
14
|
+
var ToolScopeEnum = zod.z.enum(["keys.read", "user.read", "billing.read"]);
|
|
15
|
+
var ProviderSlugEnum = zod.z.enum([
|
|
16
|
+
"openai",
|
|
17
|
+
"anthropic",
|
|
18
|
+
"google",
|
|
19
|
+
"replicate",
|
|
20
|
+
"elevenlabs",
|
|
21
|
+
"fal"
|
|
22
|
+
]);
|
|
23
|
+
var RuntimeTierEnum = zod.z.enum(["edge", "container"]);
|
|
24
|
+
var ToolOwnerSchema = zod.z.object({
|
|
25
|
+
name: zod.z.string().min(1).max(80),
|
|
26
|
+
email: zod.z.string().email().optional(),
|
|
27
|
+
github: zod.z.string().regex(/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}[a-zA-Z0-9])?$/, "github handle").optional()
|
|
28
|
+
});
|
|
29
|
+
var ToolManifestSchema = zod.z.object({
|
|
30
|
+
/** Stable URL-safe identifier. Becomes the JWT's `aud` claim. */
|
|
31
|
+
slug: zod.z.string().regex(slugRegex, "slug must be lowercase kebab-case, 3\u201340 chars"),
|
|
32
|
+
/** Human-readable name shown in the catalog and HubHeader. */
|
|
33
|
+
name: zod.z.string().min(1).max(80),
|
|
34
|
+
/** One-sentence catalog description. */
|
|
35
|
+
description: zod.z.string().max(280).default(""),
|
|
36
|
+
/** Tool version (semver). */
|
|
37
|
+
version: zod.z.string().regex(/^\d+\.\d+\.\d+(?:-[\w.+-]+)?$/, "semver"),
|
|
38
|
+
/** Runtime tier — picks template + deploy pipeline. */
|
|
39
|
+
runtime: RuntimeTierEnum,
|
|
40
|
+
/** Scopes the tool may request when minting a JWT. Min 1. */
|
|
41
|
+
scopes: zod.z.array(ToolScopeEnum).min(1),
|
|
42
|
+
/** API provider slugs the tool may fetch keys for. */
|
|
43
|
+
providers: zod.z.array(ProviderSlugEnum).default([]),
|
|
44
|
+
/** Public homepage / docs URL. Surfaced in the hub catalog. */
|
|
45
|
+
homepage: zod.z.string().url().optional(),
|
|
46
|
+
/** Where the hub sends users after issuing a token. https only. */
|
|
47
|
+
callback_url: zod.z.string().url().refine((u) => u.startsWith("https://"), "callback_url must be https://"),
|
|
48
|
+
/** Tool owner. */
|
|
49
|
+
owner: ToolOwnerSchema,
|
|
50
|
+
/** Semver range the tool was built against. Hub uses this for compat warnings. */
|
|
51
|
+
sdk_version: zod.z.string().min(1),
|
|
52
|
+
/** Optional category for catalog grouping (e.g. "media", "writing"). */
|
|
53
|
+
category: zod.z.string().min(1).max(40).optional()
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// src/scanner.ts
|
|
57
|
+
var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
58
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
59
|
+
"node_modules",
|
|
60
|
+
"dist",
|
|
61
|
+
".next",
|
|
62
|
+
".open-next",
|
|
63
|
+
".turbo",
|
|
64
|
+
"build",
|
|
65
|
+
"out",
|
|
66
|
+
"coverage",
|
|
67
|
+
".git"
|
|
68
|
+
]);
|
|
69
|
+
var PATTERNS = {
|
|
70
|
+
/** ESM/CJS import OR a `require()` of @openkeyai/ui. */
|
|
71
|
+
uiImport: /\bfrom\s+['"]@openkeyai\/ui(?:\/[^'"]+)?['"]|require\(\s*['"]@openkeyai\/ui(?:\/[^'"]+)?['"]/,
|
|
72
|
+
/** Mounted JSX: `<HubHeader …` (open tag). */
|
|
73
|
+
hubHeaderJsx: /<\s*HubHeader\b/,
|
|
74
|
+
/** Named import containing `HubHeader`. */
|
|
75
|
+
hubHeaderImport: /\bimport\b[^;\n]*?\bHubHeader\b[^;\n]*?from\s+['"]@openkeyai\/ui/,
|
|
76
|
+
/** Any import from `@openkeyai/sdk/_internal/*` — banned. */
|
|
77
|
+
bannedInternal: /from\s+['"]@openkeyai\/sdk\/_internal(?:\/[^'"]*)?['"]|require\(\s*['"]@openkeyai\/sdk\/_internal(?:\/[^'"]*)?['"]/
|
|
78
|
+
};
|
|
79
|
+
function isLayoutFile(relPath) {
|
|
80
|
+
const segments = relPath.split(/[\\/]/);
|
|
81
|
+
const base = segments[segments.length - 1] ?? "";
|
|
82
|
+
return /^layout\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
|
|
83
|
+
}
|
|
84
|
+
function* walkSourceFiles(root, dir) {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
94
|
+
yield* walkSourceFiles(root, path.join(dir, entry.name));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!entry.isFile()) continue;
|
|
98
|
+
const idx = entry.name.lastIndexOf(".");
|
|
99
|
+
const ext = idx === -1 ? "" : entry.name.slice(idx).toLowerCase();
|
|
100
|
+
if (!SOURCE_EXTS.has(ext)) continue;
|
|
101
|
+
const absPath = path.join(dir, entry.name);
|
|
102
|
+
yield { relPath: path.relative(root, absPath), absPath };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function lineOf(text, regex) {
|
|
106
|
+
const m = regex.exec(text);
|
|
107
|
+
if (!m) return void 0;
|
|
108
|
+
const upto = text.slice(0, m.index);
|
|
109
|
+
return upto.split("\n").length;
|
|
110
|
+
}
|
|
111
|
+
function scanToolSource(input) {
|
|
112
|
+
const violations = [];
|
|
113
|
+
let scannedFileCount = 0;
|
|
114
|
+
let foundHubHeaderImport = false;
|
|
115
|
+
let foundHubHeaderMount = false;
|
|
116
|
+
let firstLayoutFile;
|
|
117
|
+
let srcExists = false;
|
|
118
|
+
try {
|
|
119
|
+
fs.statSync(input.srcDir);
|
|
120
|
+
srcExists = true;
|
|
121
|
+
} catch {
|
|
122
|
+
violations.push({
|
|
123
|
+
code: "io-error",
|
|
124
|
+
message: `srcDir does not exist: ${input.srcDir}`
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (srcExists) {
|
|
128
|
+
for (const { relPath, absPath } of walkSourceFiles(
|
|
129
|
+
input.srcDir,
|
|
130
|
+
input.srcDir
|
|
131
|
+
)) {
|
|
132
|
+
scannedFileCount += 1;
|
|
133
|
+
let body;
|
|
134
|
+
try {
|
|
135
|
+
body = fs.readFileSync(absPath, "utf8");
|
|
136
|
+
} catch (err) {
|
|
137
|
+
violations.push({
|
|
138
|
+
code: "io-error",
|
|
139
|
+
message: `Could not read file: ${err instanceof Error ? err.message : "unknown"}`,
|
|
140
|
+
file: relPath
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const bannedLine = lineOf(body, PATTERNS.bannedInternal);
|
|
145
|
+
if (bannedLine !== void 0) {
|
|
146
|
+
violations.push({
|
|
147
|
+
code: "banned-internal-import",
|
|
148
|
+
message: "@openkeyai/sdk/_internal is not a public surface and may change without notice. Import only from the package root.",
|
|
149
|
+
file: relPath,
|
|
150
|
+
line: bannedLine
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (isLayoutFile(relPath)) {
|
|
154
|
+
if (firstLayoutFile === void 0) firstLayoutFile = relPath;
|
|
155
|
+
if (PATTERNS.hubHeaderImport.test(body)) {
|
|
156
|
+
foundHubHeaderImport = true;
|
|
157
|
+
}
|
|
158
|
+
if (PATTERNS.hubHeaderJsx.test(body)) {
|
|
159
|
+
foundHubHeaderMount = true;
|
|
160
|
+
}
|
|
161
|
+
} else if (PATTERNS.uiImport.test(body) && PATTERNS.hubHeaderJsx.test(body)) {
|
|
162
|
+
foundHubHeaderMount = true;
|
|
163
|
+
if (PATTERNS.hubHeaderImport.test(body)) {
|
|
164
|
+
foundHubHeaderImport = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!foundHubHeaderImport) {
|
|
169
|
+
violations.push({
|
|
170
|
+
code: "missing-hubheader-import",
|
|
171
|
+
message: "No layout file imports `HubHeader` from `@openkeyai/ui`. Every tool must mount the shared header.",
|
|
172
|
+
file: firstLayoutFile ?? "app/layout.tsx"
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (!foundHubHeaderMount) {
|
|
176
|
+
violations.push({
|
|
177
|
+
code: "missing-hubheader-mount",
|
|
178
|
+
message: "`<HubHeader />` is not mounted anywhere in the source tree. Place it in your root layout.",
|
|
179
|
+
file: firstLayoutFile ?? "app/layout.tsx"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (input.manifestPath) {
|
|
184
|
+
try {
|
|
185
|
+
const raw = fs.readFileSync(input.manifestPath, "utf8");
|
|
186
|
+
const json = JSON.parse(raw);
|
|
187
|
+
const parsed = ToolManifestSchema.safeParse(json);
|
|
188
|
+
if (!parsed.success) {
|
|
189
|
+
for (const issue of parsed.error.issues) {
|
|
190
|
+
violations.push({
|
|
191
|
+
code: "manifest-invalid",
|
|
192
|
+
message: `${issue.path.join(".") || "(root)"} \u2014 ${issue.message}`,
|
|
193
|
+
file: input.manifestPath
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
violations.push({
|
|
199
|
+
code: "manifest-missing",
|
|
200
|
+
message: `Could not read tool.json: ${err instanceof Error ? err.message : "unknown"}`,
|
|
201
|
+
file: input.manifestPath
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { violations, scannedFileCount };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/cli.ts
|
|
209
|
+
function parseArgs(argv) {
|
|
210
|
+
let srcDir = null;
|
|
211
|
+
let manifestPath = void 0;
|
|
212
|
+
for (let i = 0; i < argv.length; i++) {
|
|
213
|
+
const a = argv[i];
|
|
214
|
+
if (a === "--src" || a === "-s") {
|
|
215
|
+
const v = argv[++i];
|
|
216
|
+
if (!v) usage("missing value for --src");
|
|
217
|
+
srcDir = v;
|
|
218
|
+
} else if (a === "--manifest" || a === "-m") {
|
|
219
|
+
const v = argv[++i];
|
|
220
|
+
if (!v) usage("missing value for --manifest");
|
|
221
|
+
manifestPath = v;
|
|
222
|
+
} else if (a === "--no-manifest") {
|
|
223
|
+
manifestPath = null;
|
|
224
|
+
} else if (a === "--help" || a === "-h") {
|
|
225
|
+
printHelp();
|
|
226
|
+
process__default.default.exit(0);
|
|
227
|
+
} else if (a !== void 0) {
|
|
228
|
+
usage(`unknown argument: ${a}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const resolvedSrc = srcDir ?? defaultSrcDir();
|
|
232
|
+
const resolvedManifest = manifestPath === null ? null : manifestPath ?? (fs.existsSync("tool.json") ? "tool.json" : null);
|
|
233
|
+
return { srcDir: path.resolve(resolvedSrc), manifestPath: resolvedManifest };
|
|
234
|
+
}
|
|
235
|
+
function defaultSrcDir() {
|
|
236
|
+
if (fs.existsSync("src")) return "src";
|
|
237
|
+
if (fs.existsSync("app")) return "app";
|
|
238
|
+
return ".";
|
|
239
|
+
}
|
|
240
|
+
function usage(message) {
|
|
241
|
+
process__default.default.stderr.write(`okai-scan: ${message}
|
|
242
|
+
`);
|
|
243
|
+
printHelp(process__default.default.stderr);
|
|
244
|
+
process__default.default.exit(2);
|
|
245
|
+
}
|
|
246
|
+
function printHelp(stream = process__default.default.stdout) {
|
|
247
|
+
stream.write(
|
|
248
|
+
[
|
|
249
|
+
"Usage: okai-scan [options]",
|
|
250
|
+
"",
|
|
251
|
+
"Options:",
|
|
252
|
+
" -s, --src <dir> Source directory to scan (default: src/, then app/, then .)",
|
|
253
|
+
" -m, --manifest <path> Path to tool.json (default: ./tool.json if present)",
|
|
254
|
+
" --no-manifest Skip the manifest check entirely",
|
|
255
|
+
" -h, --help Show this help",
|
|
256
|
+
"",
|
|
257
|
+
"Exit codes:",
|
|
258
|
+
" 0 no violations",
|
|
259
|
+
" 1 one or more violations",
|
|
260
|
+
" 2 bad CLI usage",
|
|
261
|
+
""
|
|
262
|
+
].join("\n")
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
function main() {
|
|
266
|
+
const args = parseArgs(process__default.default.argv.slice(2));
|
|
267
|
+
const result = scanToolSource({
|
|
268
|
+
srcDir: args.srcDir,
|
|
269
|
+
manifestPath: args.manifestPath ?? void 0
|
|
270
|
+
});
|
|
271
|
+
if (result.violations.length === 0) {
|
|
272
|
+
process__default.default.stdout.write(
|
|
273
|
+
`okai-scan: clean (${result.scannedFileCount} source files scanned)
|
|
274
|
+
`
|
|
275
|
+
);
|
|
276
|
+
process__default.default.exit(0);
|
|
277
|
+
}
|
|
278
|
+
process__default.default.stderr.write(
|
|
279
|
+
`okai-scan: ${result.violations.length} violation(s) (${result.scannedFileCount} source files scanned)
|
|
280
|
+
|
|
281
|
+
`
|
|
282
|
+
);
|
|
283
|
+
for (const v of result.violations) {
|
|
284
|
+
const where = v.file ? `${v.file}${v.line !== void 0 ? `:${v.line}` : ""}` : "(no file)";
|
|
285
|
+
process__default.default.stderr.write(` [${v.code}] ${where}
|
|
286
|
+
${v.message}
|
|
287
|
+
|
|
288
|
+
`);
|
|
289
|
+
}
|
|
290
|
+
process__default.default.exit(1);
|
|
291
|
+
}
|
|
292
|
+
main();
|
|
293
|
+
//# sourceMappingURL=cli.cjs.map
|
|
294
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts","../src/scanner.ts","../src/cli.ts"],"names":["z","readdirSync","join","relative","statSync","readFileSync","process","existsSync","resolve"],"mappings":"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","file":"cli.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 { 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/cli.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|