@naraya/cli 0.1.0 → 0.4.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.
Files changed (83) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +394 -93
  3. package/bin/naraya-native.mjs +4 -0
  4. package/bin/naraya.mjs +1 -142
  5. package/bin/undici-timeout.mjs +1 -0
  6. package/dist/assets.pack.gz +0 -0
  7. package/dist/mcp/config-loader.js +32 -0
  8. package/dist/mcp/lifecycle.js +90 -0
  9. package/dist/mcp/tool-mapper.js +31 -0
  10. package/dist/mcp/transport.js +30 -0
  11. package/dist/pentest/catalog/catalog-loader.js +45 -0
  12. package/dist/pentest/catalog/index.js +1 -0
  13. package/dist/pentest/cli.js +117 -0
  14. package/dist/pentest/command-builder/command-builder.js +90 -0
  15. package/dist/pentest/command-builder/index.js +1 -0
  16. package/dist/pentest/index.js +10 -0
  17. package/dist/pentest/installer/index.js +1 -0
  18. package/dist/pentest/installer/tool-installer.js +90 -0
  19. package/dist/pentest/manager.js +125 -0
  20. package/dist/pentest/mode/index.js +1 -0
  21. package/dist/pentest/mode/mode-selector.js +127 -0
  22. package/dist/pentest/selector/index.js +1 -0
  23. package/dist/pentest/selector/tool-selector.js +66 -0
  24. package/dist/pentest/skill-bridge/index.js +1 -0
  25. package/dist/pentest/skill-bridge/skill-bridge.js +66 -0
  26. package/dist/pentest/skills/generator/index.js +1 -0
  27. package/dist/pentest/skills/generator/skill-generator.js +310 -0
  28. package/dist/pentest/skills/index.js +3 -0
  29. package/dist/pentest/skills/loader/index.js +1 -0
  30. package/dist/pentest/skills/loader/skill-loader.js +167 -0
  31. package/dist/pentest/skills/register/index.js +1 -0
  32. package/dist/pentest/skills/register/skill-register.js +162 -0
  33. package/dist/pentest/skills/types.js +1 -0
  34. package/dist/pentest/types.js +90 -0
  35. package/package.json +42 -14
  36. package/src/assets-pack.mjs +1 -0
  37. package/src/banner.mjs +5 -0
  38. package/src/clipboard.mjs +1 -0
  39. package/src/config.mjs +1 -40
  40. package/src/goodbye.mjs +7 -0
  41. package/src/login.mjs +7 -49
  42. package/src/mcp/config-loader.ts +50 -0
  43. package/src/mcp/lifecycle.ts +113 -0
  44. package/src/mcp/tool-mapper.ts +42 -0
  45. package/src/mcp/transport.ts +38 -0
  46. package/src/mcp-cli.mjs +5 -0
  47. package/src/pentest/catalog/catalog-loader.ts +55 -0
  48. package/src/pentest/catalog/index.ts +1 -0
  49. package/src/pentest/cli.ts +130 -0
  50. package/src/pentest/command-builder/command-builder.ts +109 -0
  51. package/src/pentest/command-builder/index.ts +1 -0
  52. package/src/pentest/index.ts +11 -0
  53. package/src/pentest/installer/index.ts +1 -0
  54. package/src/pentest/installer/tool-installer.ts +107 -0
  55. package/src/pentest/manager.ts +167 -0
  56. package/src/pentest/mode/index.ts +1 -0
  57. package/src/pentest/mode/mode-selector.ts +159 -0
  58. package/src/pentest/selector/index.ts +1 -0
  59. package/src/pentest/selector/tool-selector.ts +87 -0
  60. package/src/pentest/skill-bridge/index.ts +1 -0
  61. package/src/pentest/skill-bridge/skill-bridge.ts +86 -0
  62. package/src/pentest/skills/generator/index.ts +1 -0
  63. package/src/pentest/skills/generator/skill-generator.ts +373 -0
  64. package/src/pentest/skills/index.ts +4 -0
  65. package/src/pentest/skills/loader/index.ts +1 -0
  66. package/src/pentest/skills/loader/skill-loader.ts +206 -0
  67. package/src/pentest/skills/register/index.ts +1 -0
  68. package/src/pentest/skills/register/skill-register.ts +196 -0
  69. package/src/pentest/skills/types.ts +66 -0
  70. package/src/pentest/types.ts +341 -0
  71. package/src/seed.mjs +1 -36
  72. package/src/splash.mjs +4 -0
  73. package/src/status.mjs +2 -71
  74. package/assets/APPEND-SYSTEM.md +0 -9
  75. package/assets/extensions/naraya-brand.ts +0 -251
  76. package/assets/extensions/naraya-gate.ts +0 -23
  77. package/assets/naraya-logo.txt +0 -5
  78. package/assets/skills/narabuild/SKILL.md +0 -156
  79. package/assets/skills/naradroid/SKILL.md +0 -118
  80. package/assets/skills/naraexplore/SKILL.md +0 -71
  81. package/assets/skills/narafe/SKILL.md +0 -94
  82. package/assets/skills/naraplan/SKILL.md +0 -47
  83. package/assets/skills/narasearch/SKILL.md +0 -141
package/bin/naraya.mjs CHANGED
@@ -1,143 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
- import { fileURLToPath } from "node:url";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import os from "node:os";
7
- import { seed } from "../src/seed.mjs";
8
-
9
- const NARAYA_DIR = path.join(os.homedir(), ".naraya", "agent");
10
-
11
- // Subcommand dispatch — handled before pi resolution so these never depend on
12
- // the agent binary or an existing models.json.
13
- const sub = process.argv[2];
14
- if (sub === "login" || sub === "logout" || sub === "status") {
15
- // `--base <url>` (or NARAYA_BASE) selects the gateway, robust across shells.
16
- const { resolveBase } = await import("../src/config.mjs");
17
- const base = resolveBase(process.argv);
18
- if (sub === "login") {
19
- const { login } = await import("../src/login.mjs");
20
- await login(NARAYA_DIR, base);
21
- } else if (sub === "logout") {
22
- fs.rmSync(path.join(NARAYA_DIR, "models.json"), { force: true });
23
- console.log("Signed out. Revoke the 'Naraya CLI' key in your dashboard.");
24
- } else {
25
- const { status } = await import("../src/status.mjs");
26
- await status(NARAYA_DIR, base, { usd: process.argv.includes("--usd") });
27
- }
28
- process.exit(0);
29
- }
30
-
31
- // Resolve pi's bin from our own pinned dependency, never from PATH. The package
32
- // is ESM-only (exports has no "require" condition), so use import.meta.resolve
33
- // to find its main entry, walk up to the package root, then read bin from disk.
34
- function piPackageRoot() {
35
- let dir = path.dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent")));
36
- while (!fs.existsSync(path.join(dir, "package.json"))) {
37
- const up = path.dirname(dir);
38
- if (up === dir) throw new Error("could not locate @earendil-works/pi-coding-agent package root");
39
- dir = up;
40
- }
41
- return dir;
42
- }
43
- const piRoot = piPackageRoot();
44
- const piPkgPath = path.join(piRoot, "package.json");
45
- const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
46
- const piBinField = piPkg.bin;
47
- const piBinRel = typeof piBinField === "string" ? piBinField : piBinField.pi;
48
- const piBin = path.join(piRoot, piBinRel);
49
-
50
- // White-label the engine: pi reads `piConfig.name` from its own package.json and
51
- // uses it as the app name + window title (otherwise "pi"/"π"). Set it to Naraya
52
- // so the terminal title is "Naraya - …" instead of "π - …". This is pi's own
53
- // white-label mechanism; the patch is idempotent. Best-effort: a read-only
54
- // install just keeps the default title.
55
- const APP = "Naraya";
56
- function ensureRebrand() {
57
- try {
58
- if (piPkg.piConfig?.name === APP) return;
59
- piPkg.piConfig = { ...(piPkg.piConfig ?? {}), name: APP };
60
- fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2));
61
- } catch {
62
- /* read-only dependency — title stays default, everything else still works */
63
- }
64
- }
65
- ensureRebrand();
66
-
67
- // Seed bundled skills + extensions into the agent config dir (managed files only).
68
- const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
69
- seed(path.join(pkgRoot, "assets"), NARAYA_DIR);
70
-
71
- // Default to a quiet startup so pi's own header ("pi v0.79.1", resources list)
72
- // is suppressed and only the Naraya banner shows. Merge (don't clobber) the
73
- // user's settings; leave it alone if they've set it explicitly.
74
- function ensureQuietStartup() {
75
- const p = path.join(NARAYA_DIR, "settings.json");
76
- let s = {};
77
- try { s = JSON.parse(fs.readFileSync(p, "utf8")); } catch { /* none yet */ }
78
- if (s.quietStartup === undefined) {
79
- s.quietStartup = true;
80
- fs.mkdirSync(NARAYA_DIR, { recursive: true });
81
- fs.writeFileSync(p, JSON.stringify(s, null, 2));
82
- }
83
- }
84
- ensureQuietStartup();
85
-
86
- // Brand the system prompt so the model reaches for the bundled Nara skills.
87
- const appendSystem = path.join(pkgRoot, "assets", "APPEND-SYSTEM.md");
88
-
89
- // Default to the Naraya provider and restrict Ctrl+P model cycling to Naraya
90
- // models, so the experience is Naraya-only unless the user explicitly overrides.
91
- // Guarded on a configured `naraya` provider so we never inject an
92
- // unknown-provider error before `naraya login` has written models.json.
93
- function narayaDefaults(argv) {
94
- let hasNaraya = false;
95
- try {
96
- const cfg = JSON.parse(fs.readFileSync(path.join(NARAYA_DIR, "models.json"), "utf8"));
97
- hasNaraya = Boolean(cfg.providers?.naraya);
98
- } catch {
99
- /* no models.json yet — let pi handle provider resolution */
100
- }
101
- if (!hasNaraya) return [];
102
- const extra = [];
103
- if (!argv.includes("--provider")) extra.push("--provider", "naraya");
104
- if (!argv.includes("--models")) extra.push("--models", "naraya/*");
105
- return extra;
106
- }
107
-
108
- const userArgs = process.argv.slice(2);
109
-
110
- // Require sign-in before launching the agent. Skip the gate for informational
111
- // flags that work without a provider (version/help/model listing).
112
- const INFO_FLAGS = ["--version", "-v", "--help", "-h", "--list-models", "--models"];
113
- const isInfoOnly = userArgs.some((a) => INFO_FLAGS.includes(a));
114
- if (!isInfoOnly && !fs.existsSync(path.join(NARAYA_DIR, "models.json"))) {
115
- console.error("Run `naraya login` first.");
116
- process.exit(1);
117
- }
118
-
119
- // Show ONLY the bundled Nara skills: pi otherwise discovers skills from the cwd,
120
- // any ancestor `.agents/skills`, and global dirs, leaking unrelated skills into
121
- // the picker. Disable all discovery, then load just our seeded skills dir (which
122
- // also picks up anything the user drops in ~/.naraya/agent/skills).
123
- const skillArgs = userArgs.includes("--no-skills")
124
- ? []
125
- : ["--no-skills", "--skill", path.join(NARAYA_DIR, "skills")];
126
-
127
- const piArgs = [piBin, "--append-system-prompt", appendSystem, ...skillArgs, ...narayaDefaults(userArgs), ...userArgs];
128
-
129
- const child = spawn(process.execPath, piArgs, {
130
- stdio: "inherit",
131
- // PI_OFFLINE silences the "pi update available / pi.dev" startup banner (a
132
- // startup network op); model calls happen at runtime and are unaffected.
133
- // Set BOTH agent-dir env names: pi reads `<APP_NAME>_CODING_AGENT_DIR`, which
134
- // becomes NARAYA_CODING_AGENT_DIR once the rebrand patch applies, but stays
135
- // PI_CODING_AGENT_DIR if the patch couldn't write — so set both.
136
- env: {
137
- ...process.env,
138
- PI_CODING_AGENT_DIR: NARAYA_DIR,
139
- NARAYA_CODING_AGENT_DIR: NARAYA_DIR,
140
- PI_OFFLINE: "1",
141
- },
142
- });
143
- child.on("exit", (code) => process.exit(code ?? 0));
2
+ import{spawn as A}from"node:child_process";import{fileURLToPath as u,pathToFileURL as k}from"node:url";import r from"node:fs";import i from"node:path";import v from"node:os";import{seed as N,seedEntries as R}from"../src/seed.mjs";const o=i.join(v.homedir(),".naraya","agent"),n=process.argv[2];if(n==="login"||n==="logout"||n==="status"||n==="pentest"||n==="mcp"){const{resolveBase:e}=await import("../src/config.mjs"),s=e(process.argv);if(n==="login"){const{login:t}=await import("../src/login.mjs");await t(o,s)}else if(n==="logout")r.rmSync(i.join(o,"models.json"),{force:!0}),console.log("Signed out. Revoke the 'Naraya CLI' key in your dashboard.");else if(n==="status"){const{status:t}=await import("../src/status.mjs");await t(o,s,{usd:process.argv.includes("--usd")})}else if(n==="pentest"){const{pentestCLI:t}=await import("../dist/pentest/cli.js");await t(process.argv)}else if(n==="mcp"){const{mcpCLI:t,ensureBundledMcpServers:a}=await import("../src/mcp-cli.mjs");a(),await t(process.argv)}process.exit(0)}function P(){let e=i.dirname(u(import.meta.resolve("@earendil-works/pi-coding-agent")));for(;!r.existsSync(i.join(e,"package.json"));){const s=i.dirname(e);if(s===e)throw new Error("could not locate @earendil-works/pi-coding-agent package root");e=s}return e}const y=P(),g=i.join(y,"package.json"),p=JSON.parse(r.readFileSync(g,"utf8")),f=p.bin,O=typeof f=="string"?f:f.pi,T=i.join(y,O),h="Naraya";function x(){try{if(p.piConfig?.name===h)return;p.piConfig={...p.piConfig??{},name:h},r.writeFileSync(g,JSON.stringify(p,null,2))}catch{}}x();const m=i.dirname(i.dirname(u(import.meta.url))),S=i.join(m,"assets");if(r.existsSync(S))N(S,o);else{const{readPack:e}=await import("../src/assets-pack.mjs");R(e(i.join(m,"dist","assets.pack.gz")),o)}function I(){const e=i.join(o,"settings.json");let s={};try{s=JSON.parse(r.readFileSync(e,"utf8"))}catch{}let t=!1;s.quietStartup===void 0&&(s.quietStartup=!0,t=!0),s.narayaThemeApplied||(s.theme="naraya",s.narayaThemeApplied=!0,t=!0),s.retry===void 0&&(s.retry={enabled:!0,maxRetries:10,baseDelayMs:1e3},t=!0),t&&(r.mkdirSync(o,{recursive:!0}),r.writeFileSync(e,JSON.stringify(s,null,2)))}I();const b=i.join(o,"SYSTEM.md"),F=i.join(o,"APPEND-SYSTEM.md");function _(e){let s=!1;try{s=!!JSON.parse(r.readFileSync(i.join(o,"models.json"),"utf8")).providers?.naraya}catch{}if(!s)return[];const t=[];return e.includes("--provider")||t.push("--provider","naraya"),e.includes("--models")||t.push("--models","naraya/*"),t}const l=process.argv.slice(2),B=["--version","-v","--help","-h","--list-models","--models"],d=l.some(e=>B.includes(e));if(!d)if(process.stdout.isTTY){const{splash:e}=await import("../src/splash.mjs"),s=await e();if(!s.ok)if(s.reason==="login"||s.reason==="expired"){const{resolveBase:t}=await import("../src/config.mjs"),{login:a}=await import("../src/login.mjs");try{await a(o,t(process.argv))}catch(c){console.error(`Login gagal: ${c.message}`),process.exit(1)}}else process.exit(1)}else r.existsSync(i.join(o,"models.json"))||(console.error("Run `naraya login` first."),process.exit(1));if(!d)try{const e=i.join(o,"models.json"),t=JSON.parse(r.readFileSync(e,"utf8")).providers?.naraya;if(t?.apiKey&&t?.baseUrl){const a=await fetch(`${t.baseUrl}/models`,{headers:{authorization:`Bearer ${t.apiKey}`},signal:AbortSignal.timeout(3e3)});if(a.ok){const c=(await a.json()).data;if(Array.isArray(c)&&c.length>0){const{writeModelsConfig:j}=await import("../src/config.mjs");j(o,t.apiKey,c,t.baseUrl)}}}}catch{}if(!d)try{const{ensureBundledMcpServers:e}=await import("../src/mcp-cli.mjs");e()}catch{}if(!d&&process.stdout.isTTY)try{const{printBanner:e}=await import("../src/banner.mjs");await e()}catch{}const w=i.join(o,"skills"),D=l.includes("--no-skills")?[]:r.existsSync(w)?["--no-skills","--skill",w]:["--no-skills"],C=k(i.join(i.dirname(u(import.meta.url)),"undici-timeout.mjs")).href,E=l.includes("--system-prompt")?[]:["--system-prompt",b],Y=["--import",C,T,...E,"--append-system-prompt",F,...D,..._(l),...l],L=A(process.execPath,Y,{stdio:"inherit",env:{...process.env,PI_CODING_AGENT_DIR:o,NARAYA_CODING_AGENT_DIR:o,PI_OFFLINE:"1",NARAYA_PKG_ROOT:m,NARAYA_VERSION:(()=>{try{return JSON.parse(r.readFileSync(i.join(m,"package.json"),"utf8")).version??""}catch{return""}})()}});L.on("exit",async e=>{if(process.stdout.isTTY&&!d)try{const{printBye:s}=await import("../src/goodbye.mjs");s()}catch{}process.exit(e??0)});
@@ -0,0 +1 @@
1
+ import{setGlobalDispatcher as e,Agent as t}from"undici";e(new t({headersTimeout:0,bodyTimeout:0,keepAliveTimeout:6e4,keepAliveMaxTimeout:6e5,connect:{timeout:3e4}}));
Binary file
@@ -0,0 +1,32 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export function loadMcpConfig(projectDir, globalDir) {
4
+ const globalPath = path.join(globalDir, "mcp.json");
5
+ const projectPath = path.join(projectDir, ".mcp.json");
6
+ let globalConfig = { mcpServers: {} };
7
+ let projectConfig = { mcpServers: {} };
8
+ try {
9
+ if (fs.existsSync(globalPath)) {
10
+ globalConfig = JSON.parse(fs.readFileSync(globalPath, "utf8"));
11
+ }
12
+ }
13
+ catch {
14
+ // Ignore malformed global config
15
+ }
16
+ try {
17
+ if (fs.existsSync(projectPath)) {
18
+ projectConfig = JSON.parse(fs.readFileSync(projectPath, "utf8"));
19
+ }
20
+ }
21
+ catch {
22
+ // Ignore malformed project config
23
+ }
24
+ // Merge: project wins on conflict
25
+ const merged = {
26
+ mcpServers: {
27
+ ...globalConfig.mcpServers,
28
+ ...projectConfig.mcpServers
29
+ }
30
+ };
31
+ return merged;
32
+ }
@@ -0,0 +1,90 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { createTransport } from "./transport.js";
3
+ export class McpManager {
4
+ config;
5
+ servers = new Map();
6
+ projectDir;
7
+ constructor(config, projectDir) {
8
+ this.config = config;
9
+ this.projectDir = projectDir;
10
+ }
11
+ async start() {
12
+ const serverEntries = Object.entries(this.config.mcpServers ?? {});
13
+ for (const [name, serverConfig] of serverEntries) {
14
+ await this.startServer(name, serverConfig);
15
+ }
16
+ }
17
+ async startServer(name, config) {
18
+ try {
19
+ const transport = await createTransport(config, this.projectDir);
20
+ const client = new Client({ name: "naraya-cli", version: "0.2.4" }, { capabilities: {} });
21
+ const connection = {
22
+ client,
23
+ transport,
24
+ config,
25
+ tools: [],
26
+ status: "connecting"
27
+ };
28
+ this.servers.set(name, connection);
29
+ await client.connect(transport);
30
+ connection.status = "connected";
31
+ // Discover tools
32
+ const toolsResult = await client.listTools();
33
+ connection.tools = toolsResult.tools ?? [];
34
+ }
35
+ catch (err) {
36
+ const connection = this.servers.get(name);
37
+ if (connection) {
38
+ connection.status = "failed";
39
+ connection.error = err.message;
40
+ }
41
+ console.error(`[MCP] Failed to start server '${name}': ${err.message}`);
42
+ }
43
+ }
44
+ async stop() {
45
+ for (const [name, conn] of this.servers.entries()) {
46
+ try {
47
+ if (conn.status === "connected") {
48
+ await conn.client.close();
49
+ }
50
+ conn.status = "disconnected";
51
+ }
52
+ catch (err) {
53
+ console.error(`[MCP] Error stopping server '${name}': ${err.message}`);
54
+ }
55
+ }
56
+ this.servers.clear();
57
+ }
58
+ getServerStatus() {
59
+ const result = {};
60
+ for (const [name, conn] of this.servers.entries()) {
61
+ result[name] = {
62
+ status: conn.status,
63
+ toolCount: conn.tools.length,
64
+ error: conn.error
65
+ };
66
+ }
67
+ return result;
68
+ }
69
+ async callTool(serverName, toolName, params) {
70
+ const conn = this.servers.get(serverName);
71
+ if (!conn) {
72
+ throw new Error(`MCP server '${serverName}' not found`);
73
+ }
74
+ if (conn.status !== "connected") {
75
+ throw new Error(`MCP server '${serverName}' is not connected (status: ${conn.status})`);
76
+ }
77
+ return await conn.client.callTool({ name: toolName, arguments: params });
78
+ }
79
+ getTools() {
80
+ const result = [];
81
+ for (const [serverName, conn] of this.servers.entries()) {
82
+ if (conn.status === "connected") {
83
+ for (const tool of conn.tools) {
84
+ result.push({ serverName, tool });
85
+ }
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ }
@@ -0,0 +1,31 @@
1
+ export function mapMcpToolToPi(serverName, mcpTool) {
2
+ const toolName = `mcp__${serverName}__${mcpTool.name}`;
3
+ const label = `${serverName}: ${mcpTool.name}`;
4
+ const description = mcpTool.description ?? `MCP tool: ${mcpTool.name}`;
5
+ return {
6
+ name: toolName,
7
+ label,
8
+ description,
9
+ parameters: mcpTool.inputSchema ?? { type: "object", properties: {} }
10
+ };
11
+ }
12
+ export function mapMcpResultToPi(result) {
13
+ const content = result.content ?? [];
14
+ return {
15
+ content: content.map((c) => {
16
+ if (c.type === "text")
17
+ return { type: "text", text: c.text };
18
+ if (c.type === "image")
19
+ return { type: "image", data: c.data, mimeType: c.mimeType };
20
+ return { type: "text", text: JSON.stringify(c) };
21
+ }),
22
+ details: {}
23
+ };
24
+ }
25
+ export function mapMcpErrorToPi(err) {
26
+ return {
27
+ content: [{ type: "text", text: `MCP error: ${err.message}` }],
28
+ isError: true,
29
+ details: {}
30
+ };
31
+ }
@@ -0,0 +1,30 @@
1
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ export async function createTransport(config, projectDir) {
4
+ if (config.type === "http" || config.type === "streamable-http") {
5
+ if (!config.url) {
6
+ throw new Error("url is required for HTTP transport");
7
+ }
8
+ return new StreamableHTTPClientTransport(new URL(config.url), {
9
+ requestInit: {
10
+ headers: config.headers
11
+ }
12
+ });
13
+ }
14
+ // Default: stdio
15
+ if (!config.command) {
16
+ throw new Error("command is required for stdio transport");
17
+ }
18
+ const env = {
19
+ ...process.env,
20
+ ...config.env,
21
+ NARAYA_PROJECT_DIR: projectDir
22
+ };
23
+ return new StdioClientTransport({
24
+ command: config.command,
25
+ args: config.args ?? [],
26
+ env,
27
+ // Suppress MCP server startup logs by discarding stderr
28
+ stderr: "ignore"
29
+ });
30
+ }
@@ -0,0 +1,45 @@
1
+ const DEFAULT_CATALOG_PATH = "tools-catalog.json";
2
+ export async function loadToolsCatalog(options) {
3
+ const path = options?.catalogPath ?? DEFAULT_CATALOG_PATH;
4
+ const response = await fetch(`file://${process.cwd()}/${path}`);
5
+ if (!response.ok) {
6
+ throw new Error(`Failed to load tools catalog from ${path}: ${response.statusText}`);
7
+ }
8
+ return response.json();
9
+ }
10
+ export async function loadToolsCatalogFromFs(fs, options) {
11
+ const path = options?.catalogPath ?? DEFAULT_CATALOG_PATH;
12
+ const content = await fs.readFile(path, "utf-8");
13
+ return JSON.parse(content);
14
+ }
15
+ export function getToolByName(catalog, name) {
16
+ return catalog.tools.find((t) => t.tools_name === name);
17
+ }
18
+ export function getToolNames(catalog) {
19
+ return catalog.tools.map((t) => t.tools_name);
20
+ }
21
+ export function validateCatalog(catalog) {
22
+ if (typeof catalog !== "object" || catalog === null)
23
+ return false;
24
+ const c = catalog;
25
+ if (typeof c.$schema !== "string")
26
+ return false;
27
+ if (typeof c.version !== "string")
28
+ return false;
29
+ if (!Array.isArray(c.categories))
30
+ return false;
31
+ if (!Array.isArray(c.tools))
32
+ return false;
33
+ return c.tools.every(validateToolEntry);
34
+ }
35
+ function validateToolEntry(tool) {
36
+ if (typeof tool !== "object" || tool === null)
37
+ return false;
38
+ const t = tool;
39
+ return (typeof t.tools_name === "string" &&
40
+ typeof t.description === "string" &&
41
+ typeof t.category === "string" &&
42
+ typeof t.command === "object" &&
43
+ typeof t.skills_loader === "string" &&
44
+ Array.isArray(t.phase));
45
+ }
@@ -0,0 +1 @@
1
+ export * from "./catalog-loader.js";
@@ -0,0 +1,117 @@
1
+ import { PentestManager } from "./manager.js";
2
+ import { Command } from "commander";
3
+ export async function pentestCLI(argv) {
4
+ const program = new Command()
5
+ .name("naraya pentest")
6
+ .description("Pentest orchestration for authorized engagements")
7
+ .version("0.2.0");
8
+ const manager = new PentestManager();
9
+ // naraya pentest list
10
+ program
11
+ .command("list")
12
+ .description("List all available pentest skills")
13
+ .action(() => {
14
+ const manifests = manager.discoverSkills();
15
+ if (manifests.length === 0) {
16
+ console.log("No pentest skills found.");
17
+ return;
18
+ }
19
+ console.log(`Found ${manifests.length} skill(s):\n`);
20
+ for (const m of manifests) {
21
+ console.log(` * ${m.name} v${m.version} [${m.phase.join(", ")}]`);
22
+ console.log(` ${m.description}`);
23
+ console.log(` Tools: ${m.tools.join(", ")}`);
24
+ console.log(` Tags: ${m.tags.join(", ")}\n`);
25
+ }
26
+ });
27
+ // naraya pentest mode --target example.com
28
+ program
29
+ .command("mode")
30
+ .description("Auto-detect or set pentest mode")
31
+ .option("--target <target>", "Target to analyze for mode detection")
32
+ .option("--mode <mode>", "Force specific mode (auto|ctf|bug-bounty|red-team|blue-team|offensive|grey-hat)")
33
+ .action((options) => {
34
+ const result = manager.selectMode({
35
+ target: options.target,
36
+ preferred_mode: options.mode,
37
+ auto_detect: options.target !== undefined,
38
+ });
39
+ console.log(`\nMode: ${result.selected_mode}`);
40
+ console.log(`Auto-detected: ${result.auto_detected ? "Yes" : "No"}`);
41
+ console.log(`Reason: ${result.detection_reason}`);
42
+ console.log(`\nConfig:`);
43
+ console.log(` Description: ${result.config.description}`);
44
+ console.log(` Parallelism: ${result.config.parallelism}`);
45
+ console.log(` Stealth: ${result.config.stealth ? "Yes" : "No"}`);
46
+ console.log(` Report Format: ${result.config.report_format}`);
47
+ console.log(` Skill Chain: ${result.config.skill_chain.join(" -> ")}`);
48
+ console.log(` Tool Priority: ${result.config.tool_priority.join(" -> ")}\n`);
49
+ });
50
+ // naraya pentest phase --phase recon
51
+ program
52
+ .command("phase")
53
+ .description("Run a pentest phase")
54
+ .option("--target <url>", "Target domain or IP")
55
+ .option("--phase <phase>", "Phase: recon, enumeration, exploitation, reporting")
56
+ .option("--mode <mode>", "Pentest mode")
57
+ .option("--passive", "Passive-only mode")
58
+ .action((options) => {
59
+ if (!options.target) {
60
+ console.error("Error: --target is required");
61
+ process.exit(1);
62
+ }
63
+ if (!options.phase) {
64
+ console.error("Error: --phase is required");
65
+ process.exit(1);
66
+ }
67
+ const mode = options.mode || "auto";
68
+ const modeConfig = manager.getModeConfig(mode);
69
+ console.log(`Running ${options.phase} on ${options.target}...`);
70
+ console.log(`Mode: ${mode}`);
71
+ console.log(`Stealth: ${modeConfig.stealth ? "Yes" : "No"}`);
72
+ console.log(`Parallelism: ${modeConfig.parallelism}`);
73
+ console.log(`Skills: ${modeConfig.skill_chain.join(" -> ")}`);
74
+ const skills = manager.loadSkillsByPhase(options.phase);
75
+ const loaded = skills.filter((s) => s.loaded);
76
+ if (loaded.length > 0) {
77
+ console.log(`\nLoaded ${loaded.length} skill(s):`);
78
+ for (const s of loaded) {
79
+ console.log(` * ${s.name} v${s.skill?.version}`);
80
+ }
81
+ }
82
+ else {
83
+ console.log(`\nNo skills found for phase: ${options.phase}`);
84
+ }
85
+ console.log(`\n${options.phase} completed.`);
86
+ });
87
+ // naraya pentest register
88
+ program
89
+ .command("register")
90
+ .description("Register skills with the skill register")
91
+ .option("--skill <name>", "Register specific skill")
92
+ .option("--all", "Register all discovered skills")
93
+ .action((options) => {
94
+ const register = manager.getRegister();
95
+ if (options.all) {
96
+ const manifests = manager.discoverSkills();
97
+ const entries = register.registerFromFiles(manifests.map((m) => m.name));
98
+ console.log(`Registered ${entries.length} skill(s)`);
99
+ }
100
+ else if (options.skill) {
101
+ const entry = register.registerFromFile(options.skill);
102
+ if (entry) {
103
+ console.log(`Registered: ${entry.name} v${entry.skill.version}`);
104
+ }
105
+ else {
106
+ console.error(`Skill not found: ${options.skill}`);
107
+ process.exit(1);
108
+ }
109
+ }
110
+ else {
111
+ console.log("Use --skill <name> or --all to register skills");
112
+ }
113
+ console.log(`\nTotal registered: ${register.count()}`);
114
+ console.log(`Enabled: ${register.enabledCount()}`);
115
+ });
116
+ program.parse(argv);
117
+ }
@@ -0,0 +1,90 @@
1
+ export function buildCommand(tool, options = {}) {
2
+ const args = [];
3
+ const flags = options.flags ?? {};
4
+ const positional = options.positional ?? [];
5
+ for (const flagDef of tool.command.flags) {
6
+ const value = flags[flagDef.name];
7
+ const argValue = buildFlagArg(flagDef, value);
8
+ if (argValue) {
9
+ args.push(...argValue);
10
+ }
11
+ }
12
+ if (positional.length > 0) {
13
+ args.push(...positional);
14
+ }
15
+ else {
16
+ for (const posDef of tool.command.positional) {
17
+ const value = flags[posDef.name];
18
+ if (value !== undefined) {
19
+ args.push(String(value));
20
+ }
21
+ }
22
+ }
23
+ let fullCommand = `${tool.command.base} ${args.join(" ")}`.trim();
24
+ if (options.pipe_to && options.pipe_to.length > 0) {
25
+ fullCommand += ` | ${options.pipe_to.join(" | ")}`;
26
+ }
27
+ return {
28
+ command: tool.command.base,
29
+ args,
30
+ fullCommand,
31
+ requires_root: tool.requires_root
32
+ };
33
+ }
34
+ function buildFlagArg(flag, value) {
35
+ if (value === undefined || value === null) {
36
+ if (flag.default !== undefined && flag.type === "boolean" && flag.default === true) {
37
+ return [flag.name];
38
+ }
39
+ return null;
40
+ }
41
+ switch (flag.type) {
42
+ case "boolean":
43
+ return value === true ? [flag.name] : null;
44
+ case "string":
45
+ case "number":
46
+ return [flag.name, String(value)];
47
+ case "path":
48
+ return [flag.name, String(value)];
49
+ case "choice":
50
+ if (flag.choices && !flag.choices.includes(String(value))) {
51
+ throw new Error(`Invalid choice for ${flag.name}: ${value}. Valid: ${flag.choices.join(", ")}`);
52
+ }
53
+ return [flag.name, String(value)];
54
+ case "repeat":
55
+ if (Array.isArray(value)) {
56
+ return value.flatMap(v => [flag.name, String(v)]);
57
+ }
58
+ return [flag.name, String(value)];
59
+ default:
60
+ return null;
61
+ }
62
+ }
63
+ export function buildCommandWithSudo(tool, options = {}) {
64
+ const result = buildCommand(tool, options);
65
+ return result.requires_root ? `sudo ${result.fullCommand}` : result.fullCommand;
66
+ }
67
+ export function buildPipeline(tools, optionsPerTool) {
68
+ const commands = tools.map((tool, i) => buildCommand(tool, optionsPerTool[i]));
69
+ return commands.map(c => c.fullCommand).join(" | ");
70
+ }
71
+ export function validateRequiredFlags(tool, options) {
72
+ const errors = [];
73
+ const flags = options.flags ?? {};
74
+ for (const flag of tool.command.flags) {
75
+ if (flag.required && flags[flag.name] === undefined) {
76
+ errors.push(`Missing required flag: ${flag.name}`);
77
+ }
78
+ }
79
+ for (const pos of tool.command.positional) {
80
+ if (pos.required) {
81
+ const hasValue = options.positional?.length
82
+ ? options.positional.length > 0
83
+ : flags[pos.name] !== undefined;
84
+ if (!hasValue) {
85
+ errors.push(`Missing required positional argument: ${pos.name}`);
86
+ }
87
+ }
88
+ }
89
+ return errors;
90
+ }
@@ -0,0 +1 @@
1
+ export * from "./command-builder.js";
@@ -0,0 +1,10 @@
1
+ export * from "./catalog/index.js";
2
+ export * from "./selector/index.js";
3
+ export * from "./command-builder/index.js";
4
+ export * from "./installer/index.js";
5
+ export * from "./skill-bridge/index.js";
6
+ export * from "./mode/index.js";
7
+ // skills re-exports overlap with types - only re-export the classes/functions, not types
8
+ export { discoverSkills, loadSkill, loadAllSkills, loadSkillsByPhase, loadSkillsByTools, getSkillsForTool, resolveSkillPath as resolveSkillPathFromLoader } from "./skills/loader/skill-loader.js";
9
+ export { SkillRegister, createSkillRegister } from "./skills/register/skill-register.js";
10
+ export { generateSkill, generateAndSaveSkill, generateSkillsForPhase, generateSkillsForTool, generateFullPentestSuite, saveGeneratedSkills } from "./skills/generator/skill-generator.js";
@@ -0,0 +1 @@
1
+ export * from "./tool-installer.js";