@nkmc/cli 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @nkmc/cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for the nkmc gateway. Scan your API, generate a `skill.md`, register it with the gateway, and interact with registered services as an agent.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @nkmc/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Authenticate with the gateway (saves token to ~/.nkmc/credentials.json)
|
|
15
|
+
nkmc auth
|
|
16
|
+
|
|
17
|
+
# List services on the gateway
|
|
18
|
+
nkmc ls /
|
|
19
|
+
|
|
20
|
+
# Read a service's skill.md
|
|
21
|
+
nkmc cat /api.weather.gov/skill.md
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
### `nkmc auth`
|
|
27
|
+
|
|
28
|
+
Authenticate with the gateway. Fetches a JWT token (valid 24h) and saves it locally.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
nkmc auth
|
|
32
|
+
nkmc auth --gateway-url https://your-gateway.example.com
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
After authenticating, all `ls`/`cat`/`grep`/`write`/`rm` commands work without setting environment variables.
|
|
36
|
+
|
|
37
|
+
### `nkmc init [dir]`
|
|
38
|
+
|
|
39
|
+
Detect your project's framework and generate a `nkmc.config.ts` configuration file.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
nkmc init
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Supports Hono, Express, Fastify, and Next.js. Detects Prisma and Drizzle ORMs.
|
|
46
|
+
|
|
47
|
+
### `nkmc generate [dir]`
|
|
48
|
+
|
|
49
|
+
Scan your project and generate `.well-known/skill.md`.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
nkmc generate
|
|
53
|
+
nkmc generate --register --gateway-url https://api.nkmc.ai --domain myapi.com
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
- `--register` — Register with the gateway after generating
|
|
58
|
+
- `--gateway-url <url>` — Gateway URL for registration
|
|
59
|
+
- `--token <token>` — Auth token for registration
|
|
60
|
+
- `--domain <domain>` — Domain name for the service
|
|
61
|
+
|
|
62
|
+
### `nkmc claim <domain>`
|
|
63
|
+
|
|
64
|
+
Claim domain ownership via DNS TXT record verification.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Step 1: Request a DNS challenge
|
|
68
|
+
nkmc claim myapi.com --gateway-url https://api.nkmc.ai
|
|
69
|
+
|
|
70
|
+
# Step 2: Add the TXT record to your DNS, then verify
|
|
71
|
+
nkmc claim myapi.com --gateway-url https://api.nkmc.ai --verify
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
On success, a publish token is saved to `~/.nkmc/credentials.json` for that domain.
|
|
75
|
+
|
|
76
|
+
### `nkmc register`
|
|
77
|
+
|
|
78
|
+
Register your `skill.md` with the gateway.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
nkmc register --gateway-url https://api.nkmc.ai --domain myapi.com
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The token is resolved in this order:
|
|
85
|
+
1. `--token` flag
|
|
86
|
+
2. `NKMC_PUBLISH_TOKEN` env var
|
|
87
|
+
3. Saved publish token from `nkmc claim`
|
|
88
|
+
|
|
89
|
+
### `nkmc ls <path>`
|
|
90
|
+
|
|
91
|
+
List files/services on the gateway.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
nkmc ls / # List all services
|
|
95
|
+
nkmc ls /api.weather.gov/ # List contents of a service
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `nkmc cat <path>`
|
|
99
|
+
|
|
100
|
+
Read a file from the gateway.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
nkmc cat /api.weather.gov/skill.md
|
|
104
|
+
nkmc cat /api.weather.gov/alerts/active
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `nkmc grep <pattern> <path>`
|
|
108
|
+
|
|
109
|
+
Search across services on the gateway.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
nkmc grep "weather" /
|
|
113
|
+
nkmc grep "forecast" /api.weather.gov/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `nkmc write <path> <data>`
|
|
117
|
+
|
|
118
|
+
Write data to a path on the gateway.
|
|
119
|
+
|
|
120
|
+
### `nkmc rm <path>`
|
|
121
|
+
|
|
122
|
+
Remove a file on the gateway.
|
|
123
|
+
|
|
124
|
+
## Authentication
|
|
125
|
+
|
|
126
|
+
The CLI resolves credentials in this order:
|
|
127
|
+
|
|
128
|
+
1. `NKMC_TOKEN` / `NKMC_GATEWAY_URL` environment variables
|
|
129
|
+
2. Saved agent token from `nkmc auth` (`~/.nkmc/credentials.json`)
|
|
130
|
+
3. Default gateway URL: `https://api.nkmc.ai`
|
|
131
|
+
|
|
132
|
+
## Configuration Directory
|
|
133
|
+
|
|
134
|
+
Credentials are stored in `~/.nkmc/credentials.json` (permissions `0600`). Override the directory with the `NKMC_HOME` environment variable.
|
|
135
|
+
|
|
136
|
+
## Workflow: Registering Your API
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# 1. Initialize config
|
|
140
|
+
nkmc init
|
|
141
|
+
|
|
142
|
+
# 2. Generate skill.md
|
|
143
|
+
nkmc generate
|
|
144
|
+
|
|
145
|
+
# 3. Claim your domain
|
|
146
|
+
nkmc claim myapi.com --gateway-url https://api.nkmc.ai
|
|
147
|
+
# ... add DNS TXT record ...
|
|
148
|
+
nkmc claim myapi.com --gateway-url https://api.nkmc.ai --verify
|
|
149
|
+
|
|
150
|
+
# 4. Register with the gateway
|
|
151
|
+
nkmc register --gateway-url https://api.nkmc.ai --domain myapi.com
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -1,60 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getToken,
|
|
4
|
+
saveToken
|
|
5
|
+
} from "./chunk-MPXYSTOK.js";
|
|
2
6
|
|
|
3
7
|
// src/commands/register.ts
|
|
4
|
-
import { readFile
|
|
5
|
-
import { join as join2 } from "path";
|
|
6
|
-
|
|
7
|
-
// src/credentials.ts
|
|
8
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
8
|
+
import { readFile } from "fs/promises";
|
|
9
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
10
|
async function registerService(options) {
|
|
55
11
|
const { gatewayUrl, token, domain, skillMdPath } = options;
|
|
56
|
-
const mdPath = skillMdPath ??
|
|
57
|
-
const skillMd = await
|
|
12
|
+
const mdPath = skillMdPath ?? join(process.cwd(), ".well-known", "skill.md");
|
|
13
|
+
const skillMd = await readFile(mdPath, "utf-8");
|
|
58
14
|
if (!skillMd.trim()) {
|
|
59
15
|
throw new Error(`skill.md is empty at ${mdPath}`);
|
|
60
16
|
}
|
|
@@ -74,12 +30,35 @@ async function registerService(options) {
|
|
|
74
30
|
const result = await res.json();
|
|
75
31
|
console.log(`Registered ${result.name} as ${result.domain}`);
|
|
76
32
|
}
|
|
33
|
+
async function renewToken(gatewayUrl, domain) {
|
|
34
|
+
const baseUrl = gatewayUrl.replace(/\/$/, "");
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${baseUrl}/domains/verify`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify({ domain })
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) return null;
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
if (!data.publishToken) return null;
|
|
44
|
+
await saveToken(domain, data.publishToken);
|
|
45
|
+
console.log(`Token renewed for ${domain}`);
|
|
46
|
+
return data.publishToken;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
77
51
|
async function resolveToken(options) {
|
|
78
52
|
if (options.token) return options.token;
|
|
79
53
|
if (process.env.NKMC_PUBLISH_TOKEN) return process.env.NKMC_PUBLISH_TOKEN;
|
|
80
54
|
if (options.domain) {
|
|
81
55
|
const stored = await getToken(options.domain);
|
|
82
56
|
if (stored) return stored;
|
|
57
|
+
const gw = options.gatewayUrl ?? process.env.NKMC_GATEWAY_URL;
|
|
58
|
+
if (gw) {
|
|
59
|
+
const renewed = await renewToken(gw, options.domain);
|
|
60
|
+
if (renewed) return renewed;
|
|
61
|
+
}
|
|
83
62
|
}
|
|
84
63
|
if (options.adminToken) {
|
|
85
64
|
console.warn("Warning: --admin-token is deprecated. Use `nkmc claim` to obtain a publish token.");
|
|
@@ -94,13 +73,8 @@ async function resolveToken(options) {
|
|
|
94
73
|
}
|
|
95
74
|
async function runRegister(options) {
|
|
96
75
|
const projectDir = options.dir ?? process.cwd();
|
|
97
|
-
const gatewayUrl = options.gatewayUrl ?? process.env.NKMC_GATEWAY_URL;
|
|
76
|
+
const gatewayUrl = options.gatewayUrl ?? process.env.NKMC_GATEWAY_URL ?? "https://api.nkmc.ai";
|
|
98
77
|
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
78
|
if (!domain) {
|
|
105
79
|
throw new Error(
|
|
106
80
|
"Domain is required. Use --domain or NKMC_DOMAIN env var."
|
|
@@ -109,18 +83,18 @@ async function runRegister(options) {
|
|
|
109
83
|
const token = await resolveToken({
|
|
110
84
|
token: options.token,
|
|
111
85
|
adminToken: options.adminToken,
|
|
112
|
-
domain
|
|
86
|
+
domain,
|
|
87
|
+
gatewayUrl
|
|
113
88
|
});
|
|
114
89
|
await registerService({
|
|
115
90
|
gatewayUrl,
|
|
116
91
|
token,
|
|
117
92
|
domain,
|
|
118
|
-
skillMdPath:
|
|
93
|
+
skillMdPath: join(projectDir, ".well-known", "skill.md")
|
|
119
94
|
});
|
|
120
95
|
}
|
|
121
96
|
|
|
122
97
|
export {
|
|
123
|
-
saveToken,
|
|
124
98
|
registerService,
|
|
125
99
|
resolveToken,
|
|
126
100
|
runRegister
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/credentials.ts
|
|
4
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { chmod } from "fs/promises";
|
|
8
|
+
function nkmcDir() {
|
|
9
|
+
return process.env.NKMC_HOME || join(homedir(), ".nkmc");
|
|
10
|
+
}
|
|
11
|
+
function credentialsPath() {
|
|
12
|
+
return join(nkmcDir(), "credentials.json");
|
|
13
|
+
}
|
|
14
|
+
async function loadCredentials() {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await readFile(credentialsPath(), "utf-8");
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
return { tokens: {} };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function saveToken(domain, publishToken) {
|
|
23
|
+
const creds = await loadCredentials();
|
|
24
|
+
const payloadB64 = publishToken.split(".")[1];
|
|
25
|
+
const payload = JSON.parse(
|
|
26
|
+
Buffer.from(payloadB64, "base64url").toString("utf-8")
|
|
27
|
+
);
|
|
28
|
+
creds.tokens[domain] = {
|
|
29
|
+
publishToken,
|
|
30
|
+
issuedAt: new Date(payload.iat * 1e3).toISOString(),
|
|
31
|
+
expiresAt: new Date(payload.exp * 1e3).toISOString()
|
|
32
|
+
};
|
|
33
|
+
const dir = nkmcDir();
|
|
34
|
+
await mkdir(dir, { recursive: true });
|
|
35
|
+
const filePath = credentialsPath();
|
|
36
|
+
await writeFile(filePath, JSON.stringify(creds, null, 2) + "\n");
|
|
37
|
+
await chmod(filePath, 384);
|
|
38
|
+
}
|
|
39
|
+
async function saveAgentToken(gatewayUrl, token) {
|
|
40
|
+
const creds = await loadCredentials();
|
|
41
|
+
const payloadB64 = token.split(".")[1];
|
|
42
|
+
const payload = JSON.parse(
|
|
43
|
+
Buffer.from(payloadB64, "base64url").toString("utf-8")
|
|
44
|
+
);
|
|
45
|
+
creds.agentToken = {
|
|
46
|
+
token,
|
|
47
|
+
gatewayUrl,
|
|
48
|
+
issuedAt: new Date(payload.iat * 1e3).toISOString(),
|
|
49
|
+
expiresAt: new Date(payload.exp * 1e3).toISOString()
|
|
50
|
+
};
|
|
51
|
+
const dir = nkmcDir();
|
|
52
|
+
await mkdir(dir, { recursive: true });
|
|
53
|
+
const filePath = credentialsPath();
|
|
54
|
+
await writeFile(filePath, JSON.stringify(creds, null, 2) + "\n");
|
|
55
|
+
await chmod(filePath, 384);
|
|
56
|
+
}
|
|
57
|
+
async function getAgentToken() {
|
|
58
|
+
const creds = await loadCredentials();
|
|
59
|
+
const entry = creds.agentToken;
|
|
60
|
+
if (!entry) return null;
|
|
61
|
+
if (new Date(entry.expiresAt).getTime() < Date.now()) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return entry;
|
|
65
|
+
}
|
|
66
|
+
async function getToken(domain) {
|
|
67
|
+
const creds = await loadCredentials();
|
|
68
|
+
const entry = creds.tokens[domain];
|
|
69
|
+
if (!entry) return null;
|
|
70
|
+
if (new Date(entry.expiresAt).getTime() < Date.now()) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return entry.publishToken;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
loadCredentials,
|
|
78
|
+
saveToken,
|
|
79
|
+
saveAgentToken,
|
|
80
|
+
getAgentToken,
|
|
81
|
+
getToken
|
|
82
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
runRegister
|
|
3
|
+
runRegister
|
|
4
|
+
} from "./chunk-5ZYHHSZI.js";
|
|
5
|
+
import {
|
|
6
|
+
saveAgentToken,
|
|
4
7
|
saveToken
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MPXYSTOK.js";
|
|
6
9
|
|
|
7
10
|
// src/index.ts
|
|
8
11
|
import { Command } from "commander";
|
|
@@ -112,18 +115,137 @@ async function scanRoutes(projectDir, framework) {
|
|
|
112
115
|
}
|
|
113
116
|
return scanCodeRoutes(projectDir);
|
|
114
117
|
}
|
|
118
|
+
function extractMountCalls(project) {
|
|
119
|
+
const mounts = [];
|
|
120
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
121
|
+
const filePath = sourceFile.getFilePath();
|
|
122
|
+
const fileDir = dirname(filePath);
|
|
123
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
125
|
+
const specifier = imp.getModuleSpecifierValue();
|
|
126
|
+
const resolved = resolveModulePath(project, fileDir, specifier);
|
|
127
|
+
if (!resolved) continue;
|
|
128
|
+
for (const named of imp.getNamedImports()) {
|
|
129
|
+
const localName = named.getAliasNode()?.getText() || named.getName();
|
|
130
|
+
importMap.set(localName, resolved);
|
|
131
|
+
}
|
|
132
|
+
const defaultImport = imp.getDefaultImport();
|
|
133
|
+
if (defaultImport) {
|
|
134
|
+
importMap.set(defaultImport.getText(), resolved);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
138
|
+
for (const call of calls) {
|
|
139
|
+
const expr = call.getExpression();
|
|
140
|
+
if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) continue;
|
|
141
|
+
const propAccess = expr.asKind(SyntaxKind.PropertyAccessExpression);
|
|
142
|
+
if (!propAccess || propAccess.getName() !== "route") continue;
|
|
143
|
+
const args = call.getArguments();
|
|
144
|
+
if (args.length < 2) continue;
|
|
145
|
+
const pathArg = args[0];
|
|
146
|
+
if (pathArg.getKind() !== SyntaxKind.StringLiteral) continue;
|
|
147
|
+
const mountPath = pathArg.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
148
|
+
if (!mountPath) continue;
|
|
149
|
+
const childVar = args[1].getText();
|
|
150
|
+
const parentVar = propAccess.getExpression().getText();
|
|
151
|
+
mounts.push({
|
|
152
|
+
parentVar,
|
|
153
|
+
mountPath,
|
|
154
|
+
childVar,
|
|
155
|
+
childSourceFile: importMap.get(childVar) || null,
|
|
156
|
+
inFile: filePath
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return mounts;
|
|
161
|
+
}
|
|
162
|
+
function resolveModulePath(project, fromDir, specifier) {
|
|
163
|
+
if (!specifier.startsWith(".")) return null;
|
|
164
|
+
const base = join3(fromDir, specifier);
|
|
165
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
166
|
+
for (const ext of extensions) {
|
|
167
|
+
if (project.getSourceFile(base + ext)) return base + ext;
|
|
168
|
+
}
|
|
169
|
+
for (const ext of extensions) {
|
|
170
|
+
const indexPath = join3(base, "index" + ext);
|
|
171
|
+
if (project.getSourceFile(indexPath)) return indexPath;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function computePrefixes(mounts) {
|
|
176
|
+
const filePrefixMap = /* @__PURE__ */ new Map();
|
|
177
|
+
const varPrefixMap = /* @__PURE__ */ new Map();
|
|
178
|
+
const localChildKeys = /* @__PURE__ */ new Set();
|
|
179
|
+
for (const m of mounts) {
|
|
180
|
+
if (!m.childSourceFile) {
|
|
181
|
+
localChildKeys.add(`${m.inFile}\0${m.childVar}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const m of mounts) {
|
|
185
|
+
const parentKey = `${m.inFile}\0${m.parentVar}`;
|
|
186
|
+
if (!localChildKeys.has(parentKey)) {
|
|
187
|
+
varPrefixMap.set(parentKey, "");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let changed = true;
|
|
191
|
+
let iterations = 0;
|
|
192
|
+
while (changed && iterations < 10) {
|
|
193
|
+
changed = false;
|
|
194
|
+
iterations++;
|
|
195
|
+
for (const m of mounts) {
|
|
196
|
+
const parentKey = `${m.inFile}\0${m.parentVar}`;
|
|
197
|
+
let parentPrefix = varPrefixMap.get(parentKey);
|
|
198
|
+
if (parentPrefix === void 0 && filePrefixMap.has(m.inFile)) {
|
|
199
|
+
parentPrefix = filePrefixMap.get(m.inFile);
|
|
200
|
+
varPrefixMap.set(parentKey, parentPrefix);
|
|
201
|
+
changed = true;
|
|
202
|
+
}
|
|
203
|
+
if (parentPrefix === void 0) continue;
|
|
204
|
+
const fullPrefix = joinPaths(parentPrefix, m.mountPath);
|
|
205
|
+
if (m.childSourceFile) {
|
|
206
|
+
if (!filePrefixMap.has(m.childSourceFile)) {
|
|
207
|
+
filePrefixMap.set(m.childSourceFile, fullPrefix);
|
|
208
|
+
changed = true;
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
const childKey = `${m.inFile}\0${m.childVar}`;
|
|
212
|
+
if (!varPrefixMap.has(childKey)) {
|
|
213
|
+
varPrefixMap.set(childKey, fullPrefix);
|
|
214
|
+
changed = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { filePrefixMap, varPrefixMap };
|
|
220
|
+
}
|
|
221
|
+
function joinPaths(prefix, path) {
|
|
222
|
+
if (!prefix) return path;
|
|
223
|
+
if (path === "/") return prefix;
|
|
224
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
225
|
+
const cleanPath = path.startsWith("/") ? path : "/" + path;
|
|
226
|
+
return cleanPrefix + cleanPath;
|
|
227
|
+
}
|
|
115
228
|
async function scanCodeRoutes(projectDir) {
|
|
116
229
|
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
117
230
|
const srcFiles = await findTsFiles(projectDir);
|
|
118
231
|
for (const filePath of srcFiles) {
|
|
119
232
|
project.addSourceFileAtPath(filePath);
|
|
120
233
|
}
|
|
234
|
+
const mounts = extractMountCalls(project);
|
|
235
|
+
const { filePrefixMap, varPrefixMap } = computePrefixes(mounts);
|
|
121
236
|
const routes = [];
|
|
122
237
|
for (const sourceFile of project.getSourceFiles()) {
|
|
238
|
+
const absPath = sourceFile.getFilePath();
|
|
239
|
+
const filePrefix = filePrefixMap.get(absPath);
|
|
123
240
|
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
124
241
|
for (const call of calls) {
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
242
|
+
const result = extractRouteFromCall(call, projectDir);
|
|
243
|
+
if (!result) continue;
|
|
244
|
+
const { route, callerVar } = result;
|
|
245
|
+
const varKey = `${absPath}\0${callerVar}`;
|
|
246
|
+
const prefix = varPrefixMap.get(varKey) ?? filePrefix ?? "";
|
|
247
|
+
route.path = joinPaths(prefix, route.path);
|
|
248
|
+
routes.push(route);
|
|
127
249
|
}
|
|
128
250
|
}
|
|
129
251
|
return routes;
|
|
@@ -140,14 +262,18 @@ function extractRouteFromCall(call, projectDir) {
|
|
|
140
262
|
const firstArg = args[0];
|
|
141
263
|
if (firstArg.getKind() !== SyntaxKind.StringLiteral) return null;
|
|
142
264
|
const path = firstArg.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
143
|
-
if (!path) return null;
|
|
265
|
+
if (!path || !path.startsWith("/")) return null;
|
|
144
266
|
const description = extractLeadingComment(call);
|
|
145
267
|
const filePath = relative(projectDir, call.getSourceFile().getFilePath());
|
|
268
|
+
const callerVar = propAccess.getExpression().getText();
|
|
146
269
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
270
|
+
route: {
|
|
271
|
+
method: methodName.toUpperCase(),
|
|
272
|
+
path,
|
|
273
|
+
filePath,
|
|
274
|
+
description
|
|
275
|
+
},
|
|
276
|
+
callerVar
|
|
151
277
|
};
|
|
152
278
|
}
|
|
153
279
|
function extractLeadingComment(call) {
|
|
@@ -192,7 +318,7 @@ async function findTsFiles(dir) {
|
|
|
192
318
|
}
|
|
193
319
|
async function findFiles(dir, pattern) {
|
|
194
320
|
const results = [];
|
|
195
|
-
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".next", "dist", "build", ".git"]);
|
|
321
|
+
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".next", "dist", "build", ".git", ".wrangler", ".output", ".nuxt", ".svelte-kit", ".vercel"]);
|
|
196
322
|
async function walk(current) {
|
|
197
323
|
const entries = await readdir(current, { withFileTypes: true });
|
|
198
324
|
for (const entry of entries) {
|
|
@@ -318,7 +444,7 @@ async function runGenerate(projectDir, options) {
|
|
|
318
444
|
await writeFile2(outputPath, md);
|
|
319
445
|
console.log(`Generated ${outputPath}`);
|
|
320
446
|
if (options?.register) {
|
|
321
|
-
const { registerService, resolveToken } = await import("./register-
|
|
447
|
+
const { registerService, resolveToken } = await import("./register-7SUETZ7U.js");
|
|
322
448
|
const gatewayUrl = options.gatewayUrl ?? process.env.NKMC_GATEWAY_URL;
|
|
323
449
|
const domain = options.domain ?? process.env.NKMC_DOMAIN;
|
|
324
450
|
if (!gatewayUrl || !domain) {
|
|
@@ -409,6 +535,31 @@ Domain ${domain} verified successfully!`);
|
|
|
409
535
|
console.log(` nkmc register --domain ${domain}`);
|
|
410
536
|
}
|
|
411
537
|
|
|
538
|
+
// src/commands/auth.ts
|
|
539
|
+
async function runAuth(opts) {
|
|
540
|
+
const gatewayUrl = opts.gatewayUrl ?? process.env.NKMC_GATEWAY_URL ?? "https://api.nkmc.ai";
|
|
541
|
+
const sub = `agent-${Date.now()}`;
|
|
542
|
+
const res = await fetch(`${gatewayUrl}/auth/token`, {
|
|
543
|
+
method: "POST",
|
|
544
|
+
headers: { "Content-Type": "application/json" },
|
|
545
|
+
body: JSON.stringify({
|
|
546
|
+
sub,
|
|
547
|
+
svc: "gateway",
|
|
548
|
+
roles: ["agent"],
|
|
549
|
+
expiresIn: "24h"
|
|
550
|
+
})
|
|
551
|
+
});
|
|
552
|
+
if (!res.ok) {
|
|
553
|
+
const body = await res.text();
|
|
554
|
+
throw new Error(`Auth failed (${res.status}): ${body}`);
|
|
555
|
+
}
|
|
556
|
+
const { token } = await res.json();
|
|
557
|
+
await saveAgentToken(gatewayUrl, token);
|
|
558
|
+
console.log("Authenticated with gateway");
|
|
559
|
+
console.log(` Token saved to ~/.nkmc/credentials.json`);
|
|
560
|
+
console.log(` Gateway: ${gatewayUrl}`);
|
|
561
|
+
}
|
|
562
|
+
|
|
412
563
|
// src/gateway/client.ts
|
|
413
564
|
var GatewayClient = class {
|
|
414
565
|
constructor(gatewayUrl, token) {
|
|
@@ -432,11 +583,16 @@ var GatewayClient = class {
|
|
|
432
583
|
return res.json();
|
|
433
584
|
}
|
|
434
585
|
};
|
|
435
|
-
function createClient() {
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
586
|
+
async function createClient() {
|
|
587
|
+
const { getAgentToken } = await import("./credentials-42DL3WPT.js");
|
|
588
|
+
const stored = await getAgentToken();
|
|
589
|
+
const gatewayUrl = process.env.NKMC_GATEWAY_URL ?? stored?.gatewayUrl ?? "https://api.nkmc.ai";
|
|
590
|
+
const token = process.env.NKMC_TOKEN ?? stored?.token ?? null;
|
|
591
|
+
if (!token) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
"No token found. Run 'nkmc auth' first, or set NKMC_TOKEN."
|
|
594
|
+
);
|
|
595
|
+
}
|
|
440
596
|
return new GatewayClient(gatewayUrl, token);
|
|
441
597
|
}
|
|
442
598
|
|
|
@@ -444,6 +600,34 @@ function createClient() {
|
|
|
444
600
|
function output(result) {
|
|
445
601
|
console.log(JSON.stringify(result));
|
|
446
602
|
}
|
|
603
|
+
function isSearchResults(data) {
|
|
604
|
+
if (!Array.isArray(data) || data.length === 0) return false;
|
|
605
|
+
const first = data[0];
|
|
606
|
+
return typeof first === "object" && first !== null && "domain" in first && "name" in first;
|
|
607
|
+
}
|
|
608
|
+
function isEndpointResults(data) {
|
|
609
|
+
if (!Array.isArray(data) || data.length === 0) return false;
|
|
610
|
+
const first = data[0];
|
|
611
|
+
return typeof first === "object" && first !== null && "method" in first && "path" in first;
|
|
612
|
+
}
|
|
613
|
+
function formatGrepResults(data) {
|
|
614
|
+
if (isSearchResults(data)) {
|
|
615
|
+
return data.map((s) => {
|
|
616
|
+
const header = `${s.domain} \u2014 ${s.name}`;
|
|
617
|
+
if (!s.matchedEndpoints || s.matchedEndpoints.length === 0) {
|
|
618
|
+
return header;
|
|
619
|
+
}
|
|
620
|
+
const endpoints = s.matchedEndpoints.map((e) => ` ${e.method.padEnd(6)} ${e.path} \u2014 ${e.description}`).join("\n");
|
|
621
|
+
return `${header} \xB7 ${s.matchedEndpoints.length} matched
|
|
622
|
+
${endpoints}`;
|
|
623
|
+
}).join("\n\n");
|
|
624
|
+
}
|
|
625
|
+
if (isEndpointResults(data)) {
|
|
626
|
+
if (data.length === 0) return "No matching endpoints.";
|
|
627
|
+
return data.map((e) => `${e.method.padEnd(6)} ${e.path} \u2014 ${e.description}`).join("\n");
|
|
628
|
+
}
|
|
629
|
+
return JSON.stringify(data);
|
|
630
|
+
}
|
|
447
631
|
function handleError(err) {
|
|
448
632
|
const message = err instanceof Error ? err.message : String(err);
|
|
449
633
|
console.error(JSON.stringify({ error: message }));
|
|
@@ -452,7 +636,7 @@ function handleError(err) {
|
|
|
452
636
|
function registerFsCommands(program2) {
|
|
453
637
|
program2.command("ls").description("List files in a directory").argument("<path>", "Directory path").action(async (path) => {
|
|
454
638
|
try {
|
|
455
|
-
const client = createClient();
|
|
639
|
+
const client = await createClient();
|
|
456
640
|
const result = await client.execute(`ls ${path}`);
|
|
457
641
|
output(result);
|
|
458
642
|
} catch (err) {
|
|
@@ -461,7 +645,7 @@ function registerFsCommands(program2) {
|
|
|
461
645
|
});
|
|
462
646
|
program2.command("cat").description("Read file contents").argument("<path>", "File path").action(async (path) => {
|
|
463
647
|
try {
|
|
464
|
-
const client = createClient();
|
|
648
|
+
const client = await createClient();
|
|
465
649
|
const result = await client.execute(`cat ${path}`);
|
|
466
650
|
output(result);
|
|
467
651
|
} catch (err) {
|
|
@@ -470,7 +654,7 @@ function registerFsCommands(program2) {
|
|
|
470
654
|
});
|
|
471
655
|
program2.command("write").description("Write data to a file").argument("<path>", "File path").argument("<data>", "Data to write").action(async (path, data) => {
|
|
472
656
|
try {
|
|
473
|
-
const client = createClient();
|
|
657
|
+
const client = await createClient();
|
|
474
658
|
const result = await client.execute(`write ${path} ${data}`);
|
|
475
659
|
output(result);
|
|
476
660
|
} catch (err) {
|
|
@@ -479,7 +663,7 @@ function registerFsCommands(program2) {
|
|
|
479
663
|
});
|
|
480
664
|
program2.command("rm").description("Remove a file").argument("<path>", "File path").action(async (path) => {
|
|
481
665
|
try {
|
|
482
|
-
const client = createClient();
|
|
666
|
+
const client = await createClient();
|
|
483
667
|
const result = await client.execute(`rm ${path}`);
|
|
484
668
|
output(result);
|
|
485
669
|
} catch (err) {
|
|
@@ -488,8 +672,43 @@ function registerFsCommands(program2) {
|
|
|
488
672
|
});
|
|
489
673
|
program2.command("grep").description("Search file contents").argument("<pattern>", "Search pattern").argument("<path>", "File or directory path").action(async (pattern, path) => {
|
|
490
674
|
try {
|
|
491
|
-
const client = createClient();
|
|
675
|
+
const client = await createClient();
|
|
492
676
|
const result = await client.execute(`grep ${pattern} ${path}`);
|
|
677
|
+
console.log(formatGrepResults(result));
|
|
678
|
+
} catch (err) {
|
|
679
|
+
handleError(err);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
program2.command("pipe").description("Pipe commands: cat <path> | write <path>").argument("<expression...>", "Pipe expression").action(async (expression) => {
|
|
683
|
+
try {
|
|
684
|
+
const full = expression.join(" ");
|
|
685
|
+
const parts = full.split("|").map((s) => s.trim());
|
|
686
|
+
if (parts.length !== 2) {
|
|
687
|
+
throw new Error("Pipe expression must have exactly two stages separated by '|'");
|
|
688
|
+
}
|
|
689
|
+
const [source, target] = parts;
|
|
690
|
+
if (!source.startsWith("cat ")) {
|
|
691
|
+
throw new Error("Pipe step 1 must be a 'cat' command");
|
|
692
|
+
}
|
|
693
|
+
if (!target.startsWith("write ")) {
|
|
694
|
+
throw new Error("Pipe step 2 must be a 'write' command");
|
|
695
|
+
}
|
|
696
|
+
const client = await createClient();
|
|
697
|
+
let data;
|
|
698
|
+
try {
|
|
699
|
+
data = await client.execute(source);
|
|
700
|
+
} catch (err) {
|
|
701
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
702
|
+
throw new Error(`Pipe step 1 failed: ${msg}`);
|
|
703
|
+
}
|
|
704
|
+
const writePath = target.slice("write ".length).trim();
|
|
705
|
+
let result;
|
|
706
|
+
try {
|
|
707
|
+
result = await client.execute(`write ${writePath} ${JSON.stringify(data)}`);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
710
|
+
throw new Error(`Pipe step 2 failed: ${msg}`);
|
|
711
|
+
}
|
|
493
712
|
output(result);
|
|
494
713
|
} catch (err) {
|
|
495
714
|
handleError(err);
|
|
@@ -515,12 +734,7 @@ program.command("generate").description("Scan project and generate skill.md").ar
|
|
|
515
734
|
});
|
|
516
735
|
});
|
|
517
736
|
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
|
-
}
|
|
737
|
+
const gatewayUrl = opts.gatewayUrl ?? process.env.NKMC_GATEWAY_URL ?? "https://api.nkmc.ai";
|
|
524
738
|
await runClaim({
|
|
525
739
|
gatewayUrl,
|
|
526
740
|
domain,
|
|
@@ -536,5 +750,8 @@ program.command("register").description("Register skill.md with the gateway").op
|
|
|
536
750
|
dir: opts.dir === "." ? process.cwd() : opts.dir
|
|
537
751
|
});
|
|
538
752
|
});
|
|
753
|
+
program.command("auth").description("Authenticate with the nkmc gateway").option("--gateway-url <url>", "Gateway URL (default: https://api.nkmc.ai)").action(async (opts) => {
|
|
754
|
+
await runAuth({ gatewayUrl: opts.gatewayUrl });
|
|
755
|
+
});
|
|
539
756
|
registerFsCommands(program);
|
|
540
757
|
program.parse();
|