@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.
- package/LICENSE +20 -0
- package/README.md +394 -93
- package/bin/naraya-native.mjs +4 -0
- package/bin/naraya.mjs +1 -142
- package/bin/undici-timeout.mjs +1 -0
- package/dist/assets.pack.gz +0 -0
- package/dist/mcp/config-loader.js +32 -0
- package/dist/mcp/lifecycle.js +90 -0
- package/dist/mcp/tool-mapper.js +31 -0
- package/dist/mcp/transport.js +30 -0
- package/dist/pentest/catalog/catalog-loader.js +45 -0
- package/dist/pentest/catalog/index.js +1 -0
- package/dist/pentest/cli.js +117 -0
- package/dist/pentest/command-builder/command-builder.js +90 -0
- package/dist/pentest/command-builder/index.js +1 -0
- package/dist/pentest/index.js +10 -0
- package/dist/pentest/installer/index.js +1 -0
- package/dist/pentest/installer/tool-installer.js +90 -0
- package/dist/pentest/manager.js +125 -0
- package/dist/pentest/mode/index.js +1 -0
- package/dist/pentest/mode/mode-selector.js +127 -0
- package/dist/pentest/selector/index.js +1 -0
- package/dist/pentest/selector/tool-selector.js +66 -0
- package/dist/pentest/skill-bridge/index.js +1 -0
- package/dist/pentest/skill-bridge/skill-bridge.js +66 -0
- package/dist/pentest/skills/generator/index.js +1 -0
- package/dist/pentest/skills/generator/skill-generator.js +310 -0
- package/dist/pentest/skills/index.js +3 -0
- package/dist/pentest/skills/loader/index.js +1 -0
- package/dist/pentest/skills/loader/skill-loader.js +167 -0
- package/dist/pentest/skills/register/index.js +1 -0
- package/dist/pentest/skills/register/skill-register.js +162 -0
- package/dist/pentest/skills/types.js +1 -0
- package/dist/pentest/types.js +90 -0
- package/package.json +42 -14
- package/src/assets-pack.mjs +1 -0
- package/src/banner.mjs +5 -0
- package/src/clipboard.mjs +1 -0
- package/src/config.mjs +1 -40
- package/src/goodbye.mjs +7 -0
- package/src/login.mjs +7 -49
- package/src/mcp/config-loader.ts +50 -0
- package/src/mcp/lifecycle.ts +113 -0
- package/src/mcp/tool-mapper.ts +42 -0
- package/src/mcp/transport.ts +38 -0
- package/src/mcp-cli.mjs +5 -0
- package/src/pentest/catalog/catalog-loader.ts +55 -0
- package/src/pentest/catalog/index.ts +1 -0
- package/src/pentest/cli.ts +130 -0
- package/src/pentest/command-builder/command-builder.ts +109 -0
- package/src/pentest/command-builder/index.ts +1 -0
- package/src/pentest/index.ts +11 -0
- package/src/pentest/installer/index.ts +1 -0
- package/src/pentest/installer/tool-installer.ts +107 -0
- package/src/pentest/manager.ts +167 -0
- package/src/pentest/mode/index.ts +1 -0
- package/src/pentest/mode/mode-selector.ts +159 -0
- package/src/pentest/selector/index.ts +1 -0
- package/src/pentest/selector/tool-selector.ts +87 -0
- package/src/pentest/skill-bridge/index.ts +1 -0
- package/src/pentest/skill-bridge/skill-bridge.ts +86 -0
- package/src/pentest/skills/generator/index.ts +1 -0
- package/src/pentest/skills/generator/skill-generator.ts +373 -0
- package/src/pentest/skills/index.ts +4 -0
- package/src/pentest/skills/loader/index.ts +1 -0
- package/src/pentest/skills/loader/skill-loader.ts +206 -0
- package/src/pentest/skills/register/index.ts +1 -0
- package/src/pentest/skills/register/skill-register.ts +196 -0
- package/src/pentest/skills/types.ts +66 -0
- package/src/pentest/types.ts +341 -0
- package/src/seed.mjs +1 -36
- package/src/splash.mjs +4 -0
- package/src/status.mjs +2 -71
- package/assets/APPEND-SYSTEM.md +0 -9
- package/assets/extensions/naraya-brand.ts +0 -251
- package/assets/extensions/naraya-gate.ts +0 -23
- package/assets/naraya-logo.txt +0 -5
- package/assets/skills/narabuild/SKILL.md +0 -156
- package/assets/skills/naradroid/SKILL.md +0 -118
- package/assets/skills/naraexplore/SKILL.md +0 -71
- package/assets/skills/narafe/SKILL.md +0 -94
- package/assets/skills/naraplan/SKILL.md +0 -47
- 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 {
|
|
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";
|