@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 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";
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 ?? join2(process.cwd(), ".well-known", "skill.md");
57
- const skillMd = await readFile2(mdPath, "utf-8");
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: join2(projectDir, ".well-known", "skill.md")
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
+ };
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getAgentToken,
4
+ getToken,
5
+ loadCredentials,
6
+ saveAgentToken,
7
+ saveToken
8
+ } from "./chunk-MPXYSTOK.js";
9
+ export {
10
+ getAgentToken,
11
+ getToken,
12
+ loadCredentials,
13
+ saveAgentToken,
14
+ saveToken
15
+ };
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-53LSAGFF.js";
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 route = extractRouteFromCall(call, projectDir);
126
- if (route) routes.push(route);
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
- method: methodName.toUpperCase(),
148
- path,
149
- filePath,
150
- description
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-GWQ2Z2YL.js");
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 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");
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();
@@ -3,7 +3,8 @@ import {
3
3
  registerService,
4
4
  resolveToken,
5
5
  runRegister
6
- } from "./chunk-53LSAGFF.js";
6
+ } from "./chunk-5ZYHHSZI.js";
7
+ import "./chunk-MPXYSTOK.js";
7
8
  export {
8
9
  registerService,
9
10
  resolveToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkmc/cli",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for scanning and registering APIs with the nkmc gateway",
5
5
  "license": "MIT",
6
6
  "type": "module",