@nkmc/cli 0.0.1
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/chunk-53LSAGFF.js +127 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +540 -0
- package/dist/register-GWQ2Z2YL.js +11 -0
- package/package.json +31 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/register.ts
|
|
4
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
5
|
+
import { join as join2 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/credentials.ts
|
|
8
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { chmod } from "fs/promises";
|
|
12
|
+
function nkmcDir() {
|
|
13
|
+
return process.env.NKMC_HOME || join(homedir(), ".nkmc");
|
|
14
|
+
}
|
|
15
|
+
function credentialsPath() {
|
|
16
|
+
return join(nkmcDir(), "credentials.json");
|
|
17
|
+
}
|
|
18
|
+
async function loadCredentials() {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readFile(credentialsPath(), "utf-8");
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
} catch {
|
|
23
|
+
return { tokens: {} };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function saveToken(domain, publishToken) {
|
|
27
|
+
const creds = await loadCredentials();
|
|
28
|
+
const payloadB64 = publishToken.split(".")[1];
|
|
29
|
+
const payload = JSON.parse(
|
|
30
|
+
Buffer.from(payloadB64, "base64url").toString("utf-8")
|
|
31
|
+
);
|
|
32
|
+
creds.tokens[domain] = {
|
|
33
|
+
publishToken,
|
|
34
|
+
issuedAt: new Date(payload.iat * 1e3).toISOString(),
|
|
35
|
+
expiresAt: new Date(payload.exp * 1e3).toISOString()
|
|
36
|
+
};
|
|
37
|
+
const dir = nkmcDir();
|
|
38
|
+
await mkdir(dir, { recursive: true });
|
|
39
|
+
const filePath = credentialsPath();
|
|
40
|
+
await writeFile(filePath, JSON.stringify(creds, null, 2) + "\n");
|
|
41
|
+
await chmod(filePath, 384);
|
|
42
|
+
}
|
|
43
|
+
async function getToken(domain) {
|
|
44
|
+
const creds = await loadCredentials();
|
|
45
|
+
const entry = creds.tokens[domain];
|
|
46
|
+
if (!entry) return null;
|
|
47
|
+
if (new Date(entry.expiresAt).getTime() < Date.now()) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return entry.publishToken;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/commands/register.ts
|
|
54
|
+
async function registerService(options) {
|
|
55
|
+
const { gatewayUrl, token, domain, skillMdPath } = options;
|
|
56
|
+
const mdPath = skillMdPath ?? join2(process.cwd(), ".well-known", "skill.md");
|
|
57
|
+
const skillMd = await readFile2(mdPath, "utf-8");
|
|
58
|
+
if (!skillMd.trim()) {
|
|
59
|
+
throw new Error(`skill.md is empty at ${mdPath}`);
|
|
60
|
+
}
|
|
61
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/registry/services?domain=${encodeURIComponent(domain)}`;
|
|
62
|
+
const res = await fetch(url, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${token}`,
|
|
66
|
+
"Content-Type": "text/markdown"
|
|
67
|
+
},
|
|
68
|
+
body: skillMd
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const body = await res.text();
|
|
72
|
+
throw new Error(`Registration failed (${res.status}): ${body}`);
|
|
73
|
+
}
|
|
74
|
+
const result = await res.json();
|
|
75
|
+
console.log(`Registered ${result.name} as ${result.domain}`);
|
|
76
|
+
}
|
|
77
|
+
async function resolveToken(options) {
|
|
78
|
+
if (options.token) return options.token;
|
|
79
|
+
if (process.env.NKMC_PUBLISH_TOKEN) return process.env.NKMC_PUBLISH_TOKEN;
|
|
80
|
+
if (options.domain) {
|
|
81
|
+
const stored = await getToken(options.domain);
|
|
82
|
+
if (stored) return stored;
|
|
83
|
+
}
|
|
84
|
+
if (options.adminToken) {
|
|
85
|
+
console.warn("Warning: --admin-token is deprecated. Use `nkmc claim` to obtain a publish token.");
|
|
86
|
+
return options.adminToken;
|
|
87
|
+
}
|
|
88
|
+
if (process.env.NKMC_ADMIN_TOKEN) {
|
|
89
|
+
return process.env.NKMC_ADMIN_TOKEN;
|
|
90
|
+
}
|
|
91
|
+
throw new Error(
|
|
92
|
+
"No auth token found. Use `nkmc claim <domain>` to obtain a publish token, or provide --token."
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
async function runRegister(options) {
|
|
96
|
+
const projectDir = options.dir ?? process.cwd();
|
|
97
|
+
const gatewayUrl = options.gatewayUrl ?? process.env.NKMC_GATEWAY_URL;
|
|
98
|
+
const domain = options.domain ?? process.env.NKMC_DOMAIN;
|
|
99
|
+
if (!gatewayUrl) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Gateway URL is required. Use --gateway-url or NKMC_GATEWAY_URL env var."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (!domain) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Domain is required. Use --domain or NKMC_DOMAIN env var."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const token = await resolveToken({
|
|
110
|
+
token: options.token,
|
|
111
|
+
adminToken: options.adminToken,
|
|
112
|
+
domain
|
|
113
|
+
});
|
|
114
|
+
await registerService({
|
|
115
|
+
gatewayUrl,
|
|
116
|
+
token,
|
|
117
|
+
domain,
|
|
118
|
+
skillMdPath: join2(projectDir, ".well-known", "skill.md")
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
saveToken,
|
|
124
|
+
registerService,
|
|
125
|
+
resolveToken,
|
|
126
|
+
runRegister
|
|
127
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
runRegister,
|
|
4
|
+
saveToken
|
|
5
|
+
} from "./chunk-53LSAGFF.js";
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
|
|
10
|
+
// src/scanner/detect.ts
|
|
11
|
+
import { readFile, access } from "fs/promises";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { detectFrameworkFromDeps } from "@nkmc/core/scanner";
|
|
14
|
+
var FRAMEWORK_MAP = {
|
|
15
|
+
hono: "hono",
|
|
16
|
+
next: "nextjs",
|
|
17
|
+
express: "express",
|
|
18
|
+
fastify: "fastify"
|
|
19
|
+
};
|
|
20
|
+
async function detectFramework(projectDir) {
|
|
21
|
+
const pkgPath = join(projectDir, "package.json");
|
|
22
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
23
|
+
const allDeps = {
|
|
24
|
+
...pkg.dependencies,
|
|
25
|
+
...pkg.devDependencies
|
|
26
|
+
};
|
|
27
|
+
const detected = detectFrameworkFromDeps(allDeps);
|
|
28
|
+
const framework = detected ? FRAMEWORK_MAP[detected] ?? "unknown" : "unknown";
|
|
29
|
+
let orm = "none";
|
|
30
|
+
let ormSchemaPath;
|
|
31
|
+
if (allDeps["prisma"] || allDeps["@prisma/client"]) {
|
|
32
|
+
const schemaPath = "prisma/schema.prisma";
|
|
33
|
+
try {
|
|
34
|
+
await access(join(projectDir, schemaPath));
|
|
35
|
+
orm = "prisma";
|
|
36
|
+
ormSchemaPath = schemaPath;
|
|
37
|
+
} catch {
|
|
38
|
+
orm = "prisma";
|
|
39
|
+
}
|
|
40
|
+
} else if (allDeps["drizzle-orm"]) {
|
|
41
|
+
orm = "drizzle";
|
|
42
|
+
try {
|
|
43
|
+
await access(join(projectDir, "drizzle.config.ts"));
|
|
44
|
+
ormSchemaPath = "drizzle.config.ts";
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { framework, orm, ormSchemaPath };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/commands/init.ts
|
|
52
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
53
|
+
import { join as join2 } from "path";
|
|
54
|
+
function generateConfig(options) {
|
|
55
|
+
const { name, version, roles, detected } = options;
|
|
56
|
+
const rolesStr = roles.map((r) => `"${r}"`).join(", ");
|
|
57
|
+
const lines = [
|
|
58
|
+
`import { defineConfig } from "@nkmc/core";`,
|
|
59
|
+
``,
|
|
60
|
+
`export default defineConfig({`,
|
|
61
|
+
` name: "${name}",`,
|
|
62
|
+
` version: "${version}",`,
|
|
63
|
+
` roles: [${rolesStr}],`,
|
|
64
|
+
` framework: "${detected.framework}",`
|
|
65
|
+
];
|
|
66
|
+
if (detected.orm !== "none") {
|
|
67
|
+
lines.push(` orm: "${detected.orm}",`);
|
|
68
|
+
if (detected.ormSchemaPath) {
|
|
69
|
+
lines.push(` schemaPath: "${detected.ormSchemaPath}",`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
lines.push(
|
|
73
|
+
` pricing: {`,
|
|
74
|
+
` // "POST /api/example": { cost: 0.01, token: "USDC" },`,
|
|
75
|
+
` },`,
|
|
76
|
+
`});`,
|
|
77
|
+
``
|
|
78
|
+
);
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
async function runInit(projectDir) {
|
|
82
|
+
const detected = await detectFramework(projectDir);
|
|
83
|
+
console.log(`Detected framework: ${detected.framework}`);
|
|
84
|
+
console.log(`Detected ORM: ${detected.orm}`);
|
|
85
|
+
const pkgPath = join2(projectDir, "package.json");
|
|
86
|
+
const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
|
|
87
|
+
const name = pkg.name || "my-service";
|
|
88
|
+
const config = generateConfig({
|
|
89
|
+
name,
|
|
90
|
+
version: "1.0",
|
|
91
|
+
roles: ["agent"],
|
|
92
|
+
detected
|
|
93
|
+
});
|
|
94
|
+
const configPath = join2(projectDir, "nkmc.config.ts");
|
|
95
|
+
await writeFile(configPath, config);
|
|
96
|
+
console.log(`Created ${configPath}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/commands/generate.ts
|
|
100
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir } from "fs/promises";
|
|
101
|
+
import { join as join4 } from "path";
|
|
102
|
+
import { generateSkillMd } from "@nkmc/core";
|
|
103
|
+
|
|
104
|
+
// src/scanner/routes.ts
|
|
105
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
106
|
+
import { join as join3, relative, dirname } from "path";
|
|
107
|
+
import { readdir } from "fs/promises";
|
|
108
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete"]);
|
|
109
|
+
async function scanRoutes(projectDir, framework) {
|
|
110
|
+
if (framework === "nextjs") {
|
|
111
|
+
return scanNextjsRoutes(projectDir);
|
|
112
|
+
}
|
|
113
|
+
return scanCodeRoutes(projectDir);
|
|
114
|
+
}
|
|
115
|
+
async function scanCodeRoutes(projectDir) {
|
|
116
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
117
|
+
const srcFiles = await findTsFiles(projectDir);
|
|
118
|
+
for (const filePath of srcFiles) {
|
|
119
|
+
project.addSourceFileAtPath(filePath);
|
|
120
|
+
}
|
|
121
|
+
const routes = [];
|
|
122
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
123
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
124
|
+
for (const call of calls) {
|
|
125
|
+
const route = extractRouteFromCall(call, projectDir);
|
|
126
|
+
if (route) routes.push(route);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return routes;
|
|
130
|
+
}
|
|
131
|
+
function extractRouteFromCall(call, projectDir) {
|
|
132
|
+
const expr = call.getExpression();
|
|
133
|
+
if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) return null;
|
|
134
|
+
const propAccess = expr.asKind(SyntaxKind.PropertyAccessExpression);
|
|
135
|
+
if (!propAccess) return null;
|
|
136
|
+
const methodName = propAccess.getName().toLowerCase();
|
|
137
|
+
if (!HTTP_METHODS.has(methodName)) return null;
|
|
138
|
+
const args = call.getArguments();
|
|
139
|
+
if (args.length < 1) return null;
|
|
140
|
+
const firstArg = args[0];
|
|
141
|
+
if (firstArg.getKind() !== SyntaxKind.StringLiteral) return null;
|
|
142
|
+
const path = firstArg.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
143
|
+
if (!path) return null;
|
|
144
|
+
const description = extractLeadingComment(call);
|
|
145
|
+
const filePath = relative(projectDir, call.getSourceFile().getFilePath());
|
|
146
|
+
return {
|
|
147
|
+
method: methodName.toUpperCase(),
|
|
148
|
+
path,
|
|
149
|
+
filePath,
|
|
150
|
+
description
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function extractLeadingComment(call) {
|
|
154
|
+
const statement = call.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
|
|
155
|
+
if (!statement) return void 0;
|
|
156
|
+
const leadingComments = statement.getLeadingCommentRanges();
|
|
157
|
+
if (leadingComments.length === 0) return void 0;
|
|
158
|
+
const comment = leadingComments[leadingComments.length - 1].getText();
|
|
159
|
+
const match = comment.match(/\/\*\*\s*(.*?)\s*\*\//s);
|
|
160
|
+
if (match) return match[1].replace(/\s*\*\s*/g, " ").trim();
|
|
161
|
+
const singleMatch = comment.match(/\/\/\s*(.*)/);
|
|
162
|
+
if (singleMatch) return singleMatch[1].trim();
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
async function scanNextjsRoutes(projectDir) {
|
|
166
|
+
const routes = [];
|
|
167
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
168
|
+
const routeFiles = await findFiles(projectDir, /\broute\.(ts|js)$/);
|
|
169
|
+
for (const filePath of routeFiles) {
|
|
170
|
+
const relPath = relative(projectDir, filePath);
|
|
171
|
+
const urlPath = "/" + dirname(relPath).replace(/^app\//, "");
|
|
172
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
173
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
174
|
+
if (!fn.isExported()) continue;
|
|
175
|
+
const name = fn.getName()?.toUpperCase();
|
|
176
|
+
if (name && HTTP_METHODS.has(name.toLowerCase())) {
|
|
177
|
+
const jsDocs = fn.getJsDocs();
|
|
178
|
+
const description = jsDocs.length > 0 ? jsDocs[0].getDescription().trim() : void 0;
|
|
179
|
+
routes.push({
|
|
180
|
+
method: name,
|
|
181
|
+
path: urlPath,
|
|
182
|
+
filePath: relPath,
|
|
183
|
+
description: description || void 0
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return routes;
|
|
189
|
+
}
|
|
190
|
+
async function findTsFiles(dir) {
|
|
191
|
+
return findFiles(dir, /\.(ts|tsx|js|jsx)$/);
|
|
192
|
+
}
|
|
193
|
+
async function findFiles(dir, pattern) {
|
|
194
|
+
const results = [];
|
|
195
|
+
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".next", "dist", "build", ".git"]);
|
|
196
|
+
async function walk(current) {
|
|
197
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (SKIP.has(entry.name)) continue;
|
|
200
|
+
const fullPath = join3(current, entry.name);
|
|
201
|
+
if (entry.isDirectory()) {
|
|
202
|
+
await walk(fullPath);
|
|
203
|
+
} else if (pattern.test(entry.name)) {
|
|
204
|
+
results.push(fullPath);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await walk(dir);
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/scanner/schema.ts
|
|
213
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
214
|
+
import { getSchema } from "@mrleebo/prisma-ast";
|
|
215
|
+
async function scanPrismaSchema(schemaPath) {
|
|
216
|
+
const content = await readFile3(schemaPath, "utf-8");
|
|
217
|
+
const ast = getSchema(content);
|
|
218
|
+
const tables = [];
|
|
219
|
+
let pendingTopComment = "";
|
|
220
|
+
for (const block of ast.list) {
|
|
221
|
+
if (block.type === "comment") {
|
|
222
|
+
pendingTopComment = block.text.replace(/^\/\/\/?\s*/, "").trim();
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (block.type !== "model") {
|
|
226
|
+
pendingTopComment = "";
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const fields = [];
|
|
230
|
+
let prevComment = "";
|
|
231
|
+
for (const prop of block.properties) {
|
|
232
|
+
if (prop.type === "comment") {
|
|
233
|
+
prevComment = (prop.text || "").replace(/^\/\/\/?\s*/, "").trim();
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (prop.type === "field") {
|
|
237
|
+
fields.push({
|
|
238
|
+
name: prop.name,
|
|
239
|
+
type: typeof prop.fieldType === "string" ? prop.fieldType : String(prop.fieldType),
|
|
240
|
+
description: prevComment
|
|
241
|
+
});
|
|
242
|
+
prevComment = "";
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (prop.type !== "break") {
|
|
246
|
+
prevComment = "";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
tables.push({
|
|
250
|
+
name: block.name,
|
|
251
|
+
description: pendingTopComment,
|
|
252
|
+
fields
|
|
253
|
+
});
|
|
254
|
+
pendingTopComment = "";
|
|
255
|
+
}
|
|
256
|
+
return tables;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/commands/generate.ts
|
|
260
|
+
function buildSkillDefinition(config, routes, schemas) {
|
|
261
|
+
const api = routes.map((route) => {
|
|
262
|
+
const pricingKey = `${route.method} ${route.path}`;
|
|
263
|
+
const pricing = config.pricing?.[pricingKey];
|
|
264
|
+
return {
|
|
265
|
+
method: route.method,
|
|
266
|
+
path: route.path,
|
|
267
|
+
role: "agent",
|
|
268
|
+
description: route.description || `${route.method} ${route.path}`,
|
|
269
|
+
...pricing ? { pricing } : {}
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
const schema = schemas.map((s) => ({
|
|
273
|
+
name: s.name,
|
|
274
|
+
description: s.description || `${s.name} table`,
|
|
275
|
+
read: "agent",
|
|
276
|
+
write: "agent",
|
|
277
|
+
fields: s.fields.map((f) => ({
|
|
278
|
+
name: f.name,
|
|
279
|
+
type: f.type,
|
|
280
|
+
description: f.description || ""
|
|
281
|
+
}))
|
|
282
|
+
}));
|
|
283
|
+
return {
|
|
284
|
+
frontmatter: {
|
|
285
|
+
name: config.name,
|
|
286
|
+
gateway: "nkmc",
|
|
287
|
+
version: config.version,
|
|
288
|
+
roles: config.roles
|
|
289
|
+
},
|
|
290
|
+
description: `${config.name} \u2014 powered by nkmc.`,
|
|
291
|
+
schema,
|
|
292
|
+
api
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async function runGenerate(projectDir, options) {
|
|
296
|
+
const detected = await detectFramework(projectDir);
|
|
297
|
+
console.log(`Detected: ${detected.framework} + ${detected.orm}`);
|
|
298
|
+
const routes = await scanRoutes(projectDir, detected.framework);
|
|
299
|
+
console.log(`Found ${routes.length} routes`);
|
|
300
|
+
let schemas = [];
|
|
301
|
+
if (detected.orm === "prisma" && detected.ormSchemaPath) {
|
|
302
|
+
schemas = await scanPrismaSchema(join4(projectDir, detected.ormSchemaPath));
|
|
303
|
+
console.log(`Found ${schemas.length} models`);
|
|
304
|
+
}
|
|
305
|
+
const pkg = JSON.parse(await readFile4(join4(projectDir, "package.json"), "utf-8"));
|
|
306
|
+
const config = {
|
|
307
|
+
name: pkg.name || "my-service",
|
|
308
|
+
version: "1.0",
|
|
309
|
+
roles: ["agent"],
|
|
310
|
+
framework: detected.framework,
|
|
311
|
+
orm: detected.orm !== "none" ? detected.orm : void 0
|
|
312
|
+
};
|
|
313
|
+
const skill = buildSkillDefinition(config, routes, schemas);
|
|
314
|
+
const md = generateSkillMd(skill);
|
|
315
|
+
const outputDir = join4(projectDir, ".well-known");
|
|
316
|
+
await mkdir(outputDir, { recursive: true });
|
|
317
|
+
const outputPath = join4(outputDir, "skill.md");
|
|
318
|
+
await writeFile2(outputPath, md);
|
|
319
|
+
console.log(`Generated ${outputPath}`);
|
|
320
|
+
if (options?.register) {
|
|
321
|
+
const { registerService, resolveToken } = await import("./register-GWQ2Z2YL.js");
|
|
322
|
+
const gatewayUrl = options.gatewayUrl ?? process.env.NKMC_GATEWAY_URL;
|
|
323
|
+
const domain = options.domain ?? process.env.NKMC_DOMAIN;
|
|
324
|
+
if (!gatewayUrl || !domain) {
|
|
325
|
+
console.log(
|
|
326
|
+
"Skipping registration: --gateway-url and --domain are required."
|
|
327
|
+
);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const token = await resolveToken({
|
|
331
|
+
token: options.token,
|
|
332
|
+
adminToken: options.adminToken,
|
|
333
|
+
domain
|
|
334
|
+
});
|
|
335
|
+
await registerService({
|
|
336
|
+
gatewayUrl,
|
|
337
|
+
token,
|
|
338
|
+
domain,
|
|
339
|
+
skillMdPath: outputPath
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/commands/claim.ts
|
|
345
|
+
async function runClaim(options) {
|
|
346
|
+
const { gatewayUrl, domain, verify } = options;
|
|
347
|
+
const baseUrl = gatewayUrl.replace(/\/$/, "");
|
|
348
|
+
if (verify) {
|
|
349
|
+
await verifyClaim(baseUrl, domain);
|
|
350
|
+
} else {
|
|
351
|
+
await requestChallenge(baseUrl, domain);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function requestChallenge(baseUrl, domain) {
|
|
355
|
+
const res = await fetch(`${baseUrl}/domains/challenge`, {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: { "Content-Type": "application/json" },
|
|
358
|
+
body: JSON.stringify({ domain })
|
|
359
|
+
});
|
|
360
|
+
if (!res.ok) {
|
|
361
|
+
const body = await res.text();
|
|
362
|
+
throw new Error(`Challenge request failed (${res.status}): ${body}`);
|
|
363
|
+
}
|
|
364
|
+
const data = await res.json();
|
|
365
|
+
const expires = new Date(data.expiresAt).toISOString();
|
|
366
|
+
console.log(`
|
|
367
|
+
Domain claim initiated for ${data.domain}
|
|
368
|
+
`);
|
|
369
|
+
console.log(`Add the following DNS TXT record:
|
|
370
|
+
`);
|
|
371
|
+
console.log(` Name: ${data.txtRecord}`);
|
|
372
|
+
console.log(` Value: ${data.txtValue}
|
|
373
|
+
`);
|
|
374
|
+
console.log(`This challenge expires at ${expires}.
|
|
375
|
+
`);
|
|
376
|
+
console.log(`After adding the record, verify with:`);
|
|
377
|
+
console.log(` nkmc claim ${domain} --verify
|
|
378
|
+
`);
|
|
379
|
+
console.log(`Tip: You can check propagation with:`);
|
|
380
|
+
console.log(` dig TXT ${data.txtRecord}`);
|
|
381
|
+
}
|
|
382
|
+
async function verifyClaim(baseUrl, domain) {
|
|
383
|
+
const res = await fetch(`${baseUrl}/domains/verify`, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: { "Content-Type": "application/json" },
|
|
386
|
+
body: JSON.stringify({ domain })
|
|
387
|
+
});
|
|
388
|
+
const data = await res.json();
|
|
389
|
+
if (!res.ok) {
|
|
390
|
+
const error = data.error || "Verification failed";
|
|
391
|
+
console.error(`
|
|
392
|
+
Verification failed: ${error}
|
|
393
|
+
`);
|
|
394
|
+
console.log(`Troubleshooting:`);
|
|
395
|
+
console.log(` 1. Check the record exists: dig TXT _nkmc.${domain}`);
|
|
396
|
+
console.log(` 2. DNS propagation can take up to 5 minutes`);
|
|
397
|
+
console.log(` 3. Ensure the TXT value matches exactly (including "nkmc-verify=" prefix)
|
|
398
|
+
`);
|
|
399
|
+
process.exitCode = 1;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const publishToken = data.publishToken;
|
|
403
|
+
await saveToken(domain, publishToken);
|
|
404
|
+
console.log(`
|
|
405
|
+
Domain ${domain} verified successfully!`);
|
|
406
|
+
console.log(`Publish token saved to ~/.nkmc/credentials.json
|
|
407
|
+
`);
|
|
408
|
+
console.log(`You can now register services:`);
|
|
409
|
+
console.log(` nkmc register --domain ${domain}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/gateway/client.ts
|
|
413
|
+
var GatewayClient = class {
|
|
414
|
+
constructor(gatewayUrl, token) {
|
|
415
|
+
this.gatewayUrl = gatewayUrl;
|
|
416
|
+
this.token = token;
|
|
417
|
+
}
|
|
418
|
+
async execute(command) {
|
|
419
|
+
const url = `${this.gatewayUrl.replace(/\/$/, "")}/execute`;
|
|
420
|
+
const res = await fetch(url, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: {
|
|
423
|
+
Authorization: `Bearer ${this.token}`,
|
|
424
|
+
"Content-Type": "application/json"
|
|
425
|
+
},
|
|
426
|
+
body: JSON.stringify({ command })
|
|
427
|
+
});
|
|
428
|
+
if (!res.ok) {
|
|
429
|
+
const body = await res.text();
|
|
430
|
+
throw new Error(`Gateway error ${res.status}: ${body}`);
|
|
431
|
+
}
|
|
432
|
+
return res.json();
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
function createClient() {
|
|
436
|
+
const gatewayUrl = process.env.NKMC_GATEWAY_URL;
|
|
437
|
+
const token = process.env.NKMC_TOKEN;
|
|
438
|
+
if (!gatewayUrl) throw new Error("NKMC_GATEWAY_URL is required");
|
|
439
|
+
if (!token) throw new Error("NKMC_TOKEN is required");
|
|
440
|
+
return new GatewayClient(gatewayUrl, token);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/commands/fs.ts
|
|
444
|
+
function output(result) {
|
|
445
|
+
console.log(JSON.stringify(result));
|
|
446
|
+
}
|
|
447
|
+
function handleError(err) {
|
|
448
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
449
|
+
console.error(JSON.stringify({ error: message }));
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
function registerFsCommands(program2) {
|
|
453
|
+
program2.command("ls").description("List files in a directory").argument("<path>", "Directory path").action(async (path) => {
|
|
454
|
+
try {
|
|
455
|
+
const client = createClient();
|
|
456
|
+
const result = await client.execute(`ls ${path}`);
|
|
457
|
+
output(result);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
handleError(err);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
program2.command("cat").description("Read file contents").argument("<path>", "File path").action(async (path) => {
|
|
463
|
+
try {
|
|
464
|
+
const client = createClient();
|
|
465
|
+
const result = await client.execute(`cat ${path}`);
|
|
466
|
+
output(result);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
handleError(err);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
program2.command("write").description("Write data to a file").argument("<path>", "File path").argument("<data>", "Data to write").action(async (path, data) => {
|
|
472
|
+
try {
|
|
473
|
+
const client = createClient();
|
|
474
|
+
const result = await client.execute(`write ${path} ${data}`);
|
|
475
|
+
output(result);
|
|
476
|
+
} catch (err) {
|
|
477
|
+
handleError(err);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
program2.command("rm").description("Remove a file").argument("<path>", "File path").action(async (path) => {
|
|
481
|
+
try {
|
|
482
|
+
const client = createClient();
|
|
483
|
+
const result = await client.execute(`rm ${path}`);
|
|
484
|
+
output(result);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
handleError(err);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
program2.command("grep").description("Search file contents").argument("<pattern>", "Search pattern").argument("<path>", "File or directory path").action(async (pattern, path) => {
|
|
490
|
+
try {
|
|
491
|
+
const client = createClient();
|
|
492
|
+
const result = await client.execute(`grep ${pattern} ${path}`);
|
|
493
|
+
output(result);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
handleError(err);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/index.ts
|
|
501
|
+
var program = new Command();
|
|
502
|
+
program.name("nkmc").description("nkmc SDK CLI").version("0.1.0");
|
|
503
|
+
program.command("init").description("Initialize nkmc in the current project").argument("[dir]", "Project directory", ".").action(async (dir) => {
|
|
504
|
+
const projectDir = dir === "." ? process.cwd() : dir;
|
|
505
|
+
await runInit(projectDir);
|
|
506
|
+
});
|
|
507
|
+
program.command("generate").description("Scan project and generate skill.md").argument("[dir]", "Project directory", ".").option("--register", "Register the service with the gateway after generating").option("--gateway-url <url>", "Gateway URL for registration").option("--token <token>", "Auth token for registration (publish token or admin token)").option("--admin-token <token>", "Admin token for registration (deprecated, use --token)").option("--domain <domain>", "Domain name for the service").action(async (dir, opts) => {
|
|
508
|
+
const projectDir = dir === "." ? process.cwd() : dir;
|
|
509
|
+
await runGenerate(projectDir, {
|
|
510
|
+
register: opts.register,
|
|
511
|
+
gatewayUrl: opts.gatewayUrl,
|
|
512
|
+
token: opts.token,
|
|
513
|
+
adminToken: opts.adminToken,
|
|
514
|
+
domain: opts.domain
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
program.command("claim <domain>").description("Claim domain ownership via DNS verification").option("--verify", "Verify DNS record and obtain publish token").option("--gateway-url <url>", "Gateway URL").action(async (domain, opts) => {
|
|
518
|
+
const gatewayUrl = opts.gatewayUrl ?? process.env.NKMC_GATEWAY_URL;
|
|
519
|
+
if (!gatewayUrl) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
"Gateway URL is required. Use --gateway-url or NKMC_GATEWAY_URL env var."
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
await runClaim({
|
|
525
|
+
gatewayUrl,
|
|
526
|
+
domain,
|
|
527
|
+
verify: opts.verify
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
program.command("register").description("Register skill.md with the gateway").option("--gateway-url <url>", "Gateway URL").option("--token <token>", "Auth token (publish token or admin token)").option("--admin-token <token>", "Admin token (deprecated, use --token)").option("--domain <domain>", "Domain name for the service").option("--dir <dir>", "Project directory", ".").action(async (opts) => {
|
|
531
|
+
await runRegister({
|
|
532
|
+
gatewayUrl: opts.gatewayUrl,
|
|
533
|
+
token: opts.token,
|
|
534
|
+
adminToken: opts.adminToken,
|
|
535
|
+
domain: opts.domain,
|
|
536
|
+
dir: opts.dir === "." ? process.cwd() : opts.dir
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
registerFsCommands(program);
|
|
540
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nkmc/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI for scanning and registering APIs with the nkmc gateway",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nkmc": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"dev": "tsup --watch"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@mrleebo/prisma-ast": "^0.13.1",
|
|
16
|
+
"@nkmc/core": "workspace:^",
|
|
17
|
+
"commander": "^13.0.0",
|
|
18
|
+
"ts-morph": "^27.0.2"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/chekusu/nkmc-sdk.git",
|
|
23
|
+
"directory": "packages/cli"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
]
|
|
31
|
+
}
|