@naraya/cli 0.1.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +184 -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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { loadSkill } from "../loader/skill-loader.js";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
skills_dir: ".agents/skills",
|
|
6
|
+
auto_discover: true,
|
|
7
|
+
search_paths: [".agents/skills", ".opencode/skills", ".claude/skills"],
|
|
8
|
+
};
|
|
9
|
+
export class SkillRegister {
|
|
10
|
+
entries = new Map();
|
|
11
|
+
config;
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
14
|
+
if (this.config.auto_discover) {
|
|
15
|
+
this.discoverAndRegister();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
register(skill) {
|
|
19
|
+
const entry = {
|
|
20
|
+
name: skill.name,
|
|
21
|
+
skill,
|
|
22
|
+
registered_at: new Date().toISOString(),
|
|
23
|
+
enabled: true,
|
|
24
|
+
};
|
|
25
|
+
this.entries.set(skill.name, entry);
|
|
26
|
+
return entry;
|
|
27
|
+
}
|
|
28
|
+
registerFromFile(skillName, baseDir) {
|
|
29
|
+
const result = loadSkill(skillName, baseDir);
|
|
30
|
+
if (!result.loaded || !result.skill) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return this.register(result.skill);
|
|
34
|
+
}
|
|
35
|
+
registerMany(skills) {
|
|
36
|
+
return skills.map(skill => this.register(skill));
|
|
37
|
+
}
|
|
38
|
+
registerFromFiles(skillNames, baseDir) {
|
|
39
|
+
const entries = [];
|
|
40
|
+
for (const name of skillNames) {
|
|
41
|
+
const entry = this.registerFromFile(name, baseDir);
|
|
42
|
+
if (entry)
|
|
43
|
+
entries.push(entry);
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
unregister(skillName) {
|
|
48
|
+
return this.entries.delete(skillName);
|
|
49
|
+
}
|
|
50
|
+
enable(skillName) {
|
|
51
|
+
const entry = this.entries.get(skillName);
|
|
52
|
+
if (!entry)
|
|
53
|
+
return false;
|
|
54
|
+
entry.enabled = true;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
disable(skillName) {
|
|
58
|
+
const entry = this.entries.get(skillName);
|
|
59
|
+
if (!entry)
|
|
60
|
+
return false;
|
|
61
|
+
entry.enabled = false;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
get(skillName) {
|
|
65
|
+
return this.entries.get(skillName);
|
|
66
|
+
}
|
|
67
|
+
getEnabled() {
|
|
68
|
+
return [...this.entries.values()].filter(e => e.enabled);
|
|
69
|
+
}
|
|
70
|
+
getAll() {
|
|
71
|
+
return [...this.entries.values()];
|
|
72
|
+
}
|
|
73
|
+
getByPhase(phase) {
|
|
74
|
+
return this.getEnabled().filter(e => e.skill.phase.includes(phase));
|
|
75
|
+
}
|
|
76
|
+
getByCategory(category) {
|
|
77
|
+
return this.getEnabled().filter(e => e.skill.category.includes(category));
|
|
78
|
+
}
|
|
79
|
+
getByTool(toolName) {
|
|
80
|
+
return this.getEnabled().filter(e => e.skill.tools.includes(toolName));
|
|
81
|
+
}
|
|
82
|
+
getByTag(tag) {
|
|
83
|
+
return this.getEnabled().filter(e => e.skill.tags.includes(tag));
|
|
84
|
+
}
|
|
85
|
+
has(skillName) {
|
|
86
|
+
return this.entries.has(skillName);
|
|
87
|
+
}
|
|
88
|
+
count() {
|
|
89
|
+
return this.entries.size;
|
|
90
|
+
}
|
|
91
|
+
enabledCount() {
|
|
92
|
+
return this.getEnabled().length;
|
|
93
|
+
}
|
|
94
|
+
clear() {
|
|
95
|
+
this.entries.clear();
|
|
96
|
+
}
|
|
97
|
+
saveToFile(outputPath) {
|
|
98
|
+
const path = outputPath ?? join(this.config.skills_dir, ".skill-register.json");
|
|
99
|
+
const dir = dirname(path);
|
|
100
|
+
if (!existsSync(dir)) {
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
const data = {
|
|
104
|
+
version: "1.0.0",
|
|
105
|
+
saved_at: new Date().toISOString(),
|
|
106
|
+
entries: this.getAll().map(e => ({
|
|
107
|
+
name: e.name,
|
|
108
|
+
description: e.skill.description,
|
|
109
|
+
version: e.skill.version,
|
|
110
|
+
phase: e.skill.phase,
|
|
111
|
+
category: e.skill.category,
|
|
112
|
+
tools: e.skill.tools,
|
|
113
|
+
tags: e.skill.tags,
|
|
114
|
+
enabled: e.enabled,
|
|
115
|
+
registered_at: e.registered_at,
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
119
|
+
return path;
|
|
120
|
+
}
|
|
121
|
+
loadFromFile(inputPath) {
|
|
122
|
+
const path = inputPath ?? join(this.config.skills_dir, ".skill-register.json");
|
|
123
|
+
if (!existsSync(path))
|
|
124
|
+
return 0;
|
|
125
|
+
try {
|
|
126
|
+
const content = readFileSync(path, "utf-8");
|
|
127
|
+
const data = JSON.parse(content);
|
|
128
|
+
let loaded = 0;
|
|
129
|
+
for (const entry of data.entries ?? []) {
|
|
130
|
+
const result = loadSkill(entry.name);
|
|
131
|
+
if (result.loaded && result.skill) {
|
|
132
|
+
const registered = this.register(result.skill);
|
|
133
|
+
registered.enabled = entry.enabled;
|
|
134
|
+
loaded++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return loaded;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
discoverAndRegister() {
|
|
144
|
+
for (const searchPath of this.config.search_paths) {
|
|
145
|
+
if (!existsSync(searchPath))
|
|
146
|
+
continue;
|
|
147
|
+
const entries = readdirSync(searchPath);
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const skillPath = join(searchPath, entry, "SKILL.md");
|
|
150
|
+
if (existsSync(skillPath)) {
|
|
151
|
+
const result = loadSkill(entry, searchPath);
|
|
152
|
+
if (result.loaded && result.skill) {
|
|
153
|
+
this.register(result.skill);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function createSkillRegister(config) {
|
|
161
|
+
return new SkillRegister(config);
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pentest Types - Full integration from pentest-core
|
|
3
|
+
*/
|
|
4
|
+
// ── Mode Presets ──
|
|
5
|
+
export const MODE_PRESETS = {
|
|
6
|
+
"auto": {
|
|
7
|
+
mode: "auto",
|
|
8
|
+
description: "Auto-detect mode based on target type. Smart tool priority and adaptive strategy.",
|
|
9
|
+
scope_enforcement: "moderate",
|
|
10
|
+
tool_priority: ["recon", "enumeration", "exploitation", "reporting", "utility"],
|
|
11
|
+
skill_chain: ["pentest-recon", "pentest-enum", "pentest-exploit", "pentest-report"],
|
|
12
|
+
parallelism: 4,
|
|
13
|
+
stealth: false,
|
|
14
|
+
report_format: "standard",
|
|
15
|
+
loop_config: { max_iterations: 100, iteration_timeout_ms: 300000, strategy: "adaptive", auto_continue: true, speed: "moderate" },
|
|
16
|
+
safety_constraints: { scope_strict: true, no_dos: true, no_exfiltration: true, stealth_mode: false, auto_stop_on_scope_violation: true }
|
|
17
|
+
},
|
|
18
|
+
"ctf": {
|
|
19
|
+
mode: "ctf",
|
|
20
|
+
description: "CTF challenge mode. Speed-first, parallel everything, flag-focused reporting.",
|
|
21
|
+
scope_enforcement: "none",
|
|
22
|
+
tool_priority: ["exploitation", "enumeration", "recon", "utility"],
|
|
23
|
+
skill_chain: ["ctf-recon", "ctf-exploit", "ctf-crypto", "ctf-forensics"],
|
|
24
|
+
parallelism: 8,
|
|
25
|
+
stealth: false,
|
|
26
|
+
report_format: "flag",
|
|
27
|
+
loop_config: { max_iterations: 200, iteration_timeout_ms: 120000, strategy: "adaptive", auto_continue: true, speed: "fast" },
|
|
28
|
+
safety_constraints: { scope_strict: false, no_dos: false, no_exfiltration: false, stealth_mode: false, auto_stop_on_scope_violation: false }
|
|
29
|
+
},
|
|
30
|
+
"bug-bounty": {
|
|
31
|
+
mode: "bug-bounty",
|
|
32
|
+
description: "Bug bounty mode. Scope-strict, web-focused, HackerOne/Bugcrowd report format.",
|
|
33
|
+
scope_enforcement: "strict",
|
|
34
|
+
tool_priority: ["recon", "enumeration", "exploitation", "reporting"],
|
|
35
|
+
skill_chain: ["pentest-recon", "pentest-enum", "pentest-exploit", "pentest-report"],
|
|
36
|
+
parallelism: 6,
|
|
37
|
+
stealth: false,
|
|
38
|
+
report_format: "hackerone",
|
|
39
|
+
loop_config: { max_iterations: 150, iteration_timeout_ms: 300000, strategy: "continue", auto_continue: true, speed: "moderate" },
|
|
40
|
+
safety_constraints: { scope_strict: true, no_dos: true, no_exfiltration: true, stealth_mode: false, auto_stop_on_scope_violation: true }
|
|
41
|
+
},
|
|
42
|
+
"red-team": {
|
|
43
|
+
mode: "red-team",
|
|
44
|
+
description: "Red team mode. Stealth-first, persistence, lateral movement, executive reporting.",
|
|
45
|
+
scope_enforcement: "strict",
|
|
46
|
+
tool_priority: ["recon", "exploitation", "enumeration", "reporting"],
|
|
47
|
+
skill_chain: ["red-recon", "red-exploit", "red-lateral", "red-persistence"],
|
|
48
|
+
parallelism: 2,
|
|
49
|
+
stealth: true,
|
|
50
|
+
report_format: "executive",
|
|
51
|
+
loop_config: { max_iterations: 50, iteration_timeout_ms: 600000, strategy: "continue", auto_continue: true, speed: "slow" },
|
|
52
|
+
safety_constraints: { scope_strict: true, no_dos: true, no_exfiltration: true, stealth_mode: true, auto_stop_on_scope_violation: true }
|
|
53
|
+
},
|
|
54
|
+
"blue-team": {
|
|
55
|
+
mode: "blue-team",
|
|
56
|
+
description: "Blue team mode. Detection-first, log analysis, incident response, IR reporting.",
|
|
57
|
+
scope_enforcement: "strict",
|
|
58
|
+
tool_priority: ["enumeration", "recon", "reporting", "utility"],
|
|
59
|
+
skill_chain: ["blue-detect", "blue-ir", "blue-forensics", "blue-report"],
|
|
60
|
+
parallelism: 4,
|
|
61
|
+
stealth: false,
|
|
62
|
+
report_format: "ir",
|
|
63
|
+
loop_config: { max_iterations: 100, iteration_timeout_ms: 300000, strategy: "continue", auto_continue: true, speed: "moderate" },
|
|
64
|
+
safety_constraints: { scope_strict: true, no_dos: true, no_exfiltration: true, stealth_mode: false, auto_stop_on_scope_violation: true }
|
|
65
|
+
},
|
|
66
|
+
"offensive": {
|
|
67
|
+
mode: "offensive",
|
|
68
|
+
description: "Offensive mode. Aggressive exploitation, privilege escalation, technical reporting.",
|
|
69
|
+
scope_enforcement: "strict",
|
|
70
|
+
tool_priority: ["exploitation", "enumeration", "recon", "reporting"],
|
|
71
|
+
skill_chain: ["pentest-recon", "pentest-enum", "pentest-exploit", "pentest-privesc"],
|
|
72
|
+
parallelism: 6,
|
|
73
|
+
stealth: false,
|
|
74
|
+
report_format: "technical",
|
|
75
|
+
loop_config: { max_iterations: 100, iteration_timeout_ms: 300000, strategy: "continue", auto_continue: true, speed: "moderate" },
|
|
76
|
+
safety_constraints: { scope_strict: true, no_dos: true, no_exfiltration: true, stealth_mode: false, auto_stop_on_scope_violation: true }
|
|
77
|
+
},
|
|
78
|
+
"grey-hat": {
|
|
79
|
+
mode: "grey-hat",
|
|
80
|
+
description: "Grey hat mode. Balanced offensive/defensive, moderate stealth, hybrid reporting.",
|
|
81
|
+
scope_enforcement: "moderate",
|
|
82
|
+
tool_priority: ["recon", "enumeration", "exploitation", "reporting", "utility"],
|
|
83
|
+
skill_chain: ["pentest-recon", "pentest-enum", "pentest-exploit", "pentest-report"],
|
|
84
|
+
parallelism: 4,
|
|
85
|
+
stealth: true,
|
|
86
|
+
report_format: "technical",
|
|
87
|
+
loop_config: { max_iterations: 100, iteration_timeout_ms: 300000, strategy: "adaptive", auto_continue: true, speed: "moderate" },
|
|
88
|
+
safety_constraints: { scope_strict: true, no_dos: true, no_exfiltration: true, stealth_mode: true, auto_stop_on_scope_violation: true }
|
|
89
|
+
}
|
|
90
|
+
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@naraya/cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Naraya AI coding agent - one sign-in, all models via router.naraya.ai",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@naraya/cli",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Naraya AI coding agent - one sign-in, all models via router.naraya.ai",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"naraya": "bin/naraya.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"dist/"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22.19.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"build:assets": "node scripts/build-assets.mjs",
|
|
20
|
+
"minify": "node scripts/minify.mjs",
|
|
21
|
+
"restore": "node scripts/restore-src.mjs",
|
|
22
|
+
"prepack": "node scripts/build-assets.mjs && node scripts/minify.mjs",
|
|
23
|
+
"postpack": "node scripts/restore-src.mjs"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@ast-grep/napi": "^0.43.0",
|
|
27
|
+
"@earendil-works/pi-coding-agent": "0.79.1",
|
|
28
|
+
"@earendil-works/pi-tui": "0.79.1",
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"puppeteer-core": "^25.1.0",
|
|
32
|
+
"ssh2": "^1.17.0",
|
|
33
|
+
"typescript": "^6.0.3",
|
|
34
|
+
"typescript-language-server": "^5.3.0",
|
|
35
|
+
"undici": "^7.28.0"
|
|
36
|
+
},
|
|
37
|
+
"license": "UNLICENSED",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^26.0.0",
|
|
40
|
+
"esbuild": "^0.28.1"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import f from"node:fs";import i from"node:zlib";function m(r){const t=i.gunzipSync(f.readFileSync(r)),e=JSON.parse(t.toString("utf8"));return Object.entries(e).map(([o,n])=>({rel:o,buf:Buffer.from(n,"base64")}))}export{m as readPack};
|
package/src/banner.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import x from"node:fs";import I from"node:os";import l from"node:path";import{fileURLToPath as S}from"node:url";const c="\x1B[38;2;45;123;255m",R="\x1B[48;2;45;123;255m",L="\x1B[38;2;10;29;77m",E="\x1B[48;2;10;29;77m",w="\x1B[38;2;51;102;204m",u="\x1B[1m",h="\x1B[2m",r="\x1B[0m",v=40,O=48,k=/\x1b\[[0-9;]*m/g,D=t=>t.replace(k,""),i=t=>D(t).length,U=(t,o,n)=>Math.max(o,Math.min(n,t)),b=(()=>{try{const t=l.dirname(l.dirname(S(import.meta.url)));return JSON.parse(x.readFileSync(l.join(t,"package.json"),"utf8")).version??""}catch{return process.env.NARAYA_VERSION??""}})(),$=["_____________BB___","___BBB_______BBB__","_BBBBBB______BBBBB","_BBBBBBBB____BBBBB","B_BBBBBBBB___BBBBB","BB_BBBBBBBB__BBBNN","BBB_BBBBBBB__BBNNN","BBBB__BBB____BNNNN","BBBBBB_____BBBNNNN","BBBBBB___BBBBBNNNN","BBBBB___BBBBBBNNNN","BBBBB___BBBBBBNNNN","BBBBB____BBBBBNNNN","BBBBB______BBBNNN_","BBBBB_______BBNN__","_BBBB________B____","__BBB_____________","____B_____________"],m=t=>t==="B"?c:t==="N"?L:"",j=t=>t==="B"?R:t==="N"?E:"";function G(){const t=[];for(let o=0;o<$.length;o+=2){let n="";for(let B=0;B<$[o].length;B++){const _=$[o][B],e=$[o+1]?.[B]??"_";_==="_"&&e==="_"?n+=" ":_!=="_"&&e==="_"?n+=`${m(_)}\u2580${r}`:_==="_"&&e!=="_"?n+=`${m(e)}\u2584${r}`:_===e?n+=`${m(_)}\u2588${r}`:n+=`${m(_)}${j(e)}\u2580${r}`}t.push(n)}return t}const A={N:["\u2588\u2588\u2588\u2557 \u2588\u2588\u2557","\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551","\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"],A:[" \u2588\u2588\u2588\u2588\u2588\u2557 ","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u255D"],R:["\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u255D"],C:[" \u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557"," \u255A\u2550\u2550\u2550\u2550\u2550\u255D"],L:["\u2588\u2588\u2557 ","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"],I:["\u2588\u2588\u2557","\u2588\u2588\u2551","\u2588\u2588\u2551","\u2588\u2588\u2551","\u2588\u2588\u2551","\u255A\u2550\u255D"]},P="NARA",T="CLI";function Y(){const t=[];for(let o=0;o<6;o++){let n=`${u}${c}`;for(const B of P)n+=A[B][o];n+=`${r}${u}${w}`;for(const B of T)n+=A[B][o];t.push(n+r)}return t}function q(t,o,n=3){const B=Math.max(...t.map(i),0),_=Math.max(t.length,o.length),e=Math.floor((_-t.length)/2),a=Math.floor((_-o.length)/2),s=" ".repeat(n);return Array.from({length:_},(f,g)=>`${N(t[g-e]??"",B)}${s}${o[g-a]??""}`)}function C(t,o){if(i(t)<=o)return t;const n=Math.max(0,o-1);let B="",_=0;for(let e=0;e<t.length&&_<n;){if(t[e]==="\x1B"){const a=t.slice(e).match(/^\x1b\[[0-9;]*m/);if(a){B+=a[0],e+=a[0].length;continue}}B+=t[e],e+=1,_+=1}return`${B}\u2026${r}`}const N=(t,o)=>{const n=C(t,o);return n+" ".repeat(Math.max(0,o-i(n)))};function d(t,o,n){const B=i(o),_=B>0?2:0;return`${N(t,Math.max(0,n-B-_))}${" ".repeat(_)}${o}`}function p(t,o,n,B){return d(`${c}${N(t,7)}${r}${o}`,n?`${h}${n}${r}`:"",B)}const y=t=>t>=1e6?`${(t/1e6).toFixed(1).replace(/\.0$/,"")}M`:Number(t).toLocaleString("id-ID");function F(t,o){const n=o-4,B=` ${u}NARAYA${r}${b?`${c} v${b}${r}${c}`:""}${c} `,_=`${c}\u256D\u2500${B}${"\u2500".repeat(Math.max(0,o-i(B)-3))}\u256E${r}`,e=`${c}\u2570${"\u2500".repeat(Math.max(0,o-2))}\u256F${r}`,a=f=>`${c}\u2502${r} ${N(f,n)} ${c}\u2502${r}`,s=[_];return t?(s.push(a(d(`${u}${t.email||"Naraya User"}${r}`,`${h}${t.plan||"Plan"}${r}`,n))),s.push(a(p("Saldo",t.saldoIdr,t.saldoUsd,n))),s.push(a(p("Kuota",t.kuotaMain,t.kuotaSide,n)))):(s.push(a(d(`${u}Not connected${r}`,`${h}Guest${r}`,n))),s.push(a(p("Login","naraya login","",n))),s.push(a(p("Router","router.naraya.ai","",n)))),s.push(e),s}function M(t,o){const n=Math.max(...t.map(i),0),B=" ".repeat(Math.max(0,Math.floor((o-n)/2)));return t.map(_=>B+_)}function J(){return process.env.PI_CODING_AGENT_DIR??l.join(I.homedir(),".naraya","agent")}async function V(){let t;try{if(t=JSON.parse(x.readFileSync(l.join(J(),"models.json"),"utf8")).providers?.naraya,!t?.baseUrl||!t?.apiKey)return null}catch{return null}try{const o=await fetch(`${t.baseUrl}/me`,{headers:{authorization:`Bearer ${t.apiKey}`},signal:AbortSignal.timeout(3e3)});if(!o.ok)return null;const n=await o.json(),B=Number(n.credit?.available??0),_=Number(n.quota?.limit??0),e=Number(n.quota?.remaining??0);return{email:n.account?.email??"",plan:n.account?.plan??"",saldoIdr:`Rp ${B.toLocaleString("id-ID",{maximumFractionDigits:0})}`,saldoUsd:n.credit?.usd_equivalent?`$${n.credit.usd_equivalent}`:"",kuotaMain:_>0?`${y(e)} / ${y(_)}`:"fair-use",kuotaSide:`${Array.isArray(n.models)?n.models.length:0} model`}}catch{return null}}async function H(){try{if(!process.stdout.isTTY)return;const t=Number(process.stdout.columns)>0?process.stdout.columns:90,o=await V(),n=q(G(),Y(),3),B=F(o,U(t-4,v,O)),_=[...M(n,t),"",...M(B,t)];process.stdout.write(`\x1B[2J\x1B[3J\x1B[H
|
|
2
|
+
`+_.join(`
|
|
3
|
+
`)+`
|
|
4
|
+
|
|
5
|
+
`)}catch{}}export{H as printBanner};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import c from"node:os";import m from"node:path";import n from"node:fs";import p from"node:crypto";import{spawnSync as l}from"node:child_process";function i(t,o,e={}){try{const r=l(t,o,{timeout:5e3,maxBuffer:67108864,...e});return{ok:r.status===0&&!r.error,stdout:r.stdout??Buffer.alloc(0)}}catch{return{ok:!1,stdout:Buffer.alloc(0)}}}function a(){return m.join(c.tmpdir(),`naraya-clipboard-${p.randomUUID()}.png`)}function f(){const t=a(),e=["Add-Type -AssemblyName System.Windows.Forms","Add-Type -AssemblyName System.Drawing","$img = [System.Windows.Forms.Clipboard]::GetImage()",`if ($img) { $img.Save('${t.replace(/'/g,"''")}', [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }`].join("; "),r=i("powershell.exe",["-NoProfile","-NonInteractive","-Command",e]);if(!r.ok||r.stdout.toString("utf8").trim()!=="ok"){try{n.rmSync(t,{force:!0})}catch{}return null}try{if(n.statSync(t).size===0)return n.rmSync(t,{force:!0}),null}catch{return null}return{path:t,mimeType:"image/png"}}function d(){const t=i("xclip",["-selection","clipboard","-t","TARGETS","-o"]),e=(t.ok?t.stdout.toString("utf8").split(/\r?\n/).map(s=>s.trim()):[]).find(s=>/^image\//.test(s))??"image/png",r=i("xclip",["-selection","clipboard","-t",e,"-o"]);if(!r.ok||r.stdout.length===0)return null;const u=a();try{n.writeFileSync(u,r.stdout)}catch{return null}return{path:u,mimeType:e.split(";")[0]}}function y(){const t=a(),o=i("pngpaste",[t]);try{if(!o.ok||n.statSync(t).size===0)return n.rmSync(t,{force:!0}),null}catch{return null}return{path:t,mimeType:"image/png"}}function b(){try{if(process.platform==="win32")return f();if(process.platform==="darwin")return y();if(process.platform==="linux")return d()}catch{}return null}export{b as readClipboardImage};
|
package/src/config.mjs
CHANGED
|
@@ -1,40 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const DEFAULT_BASE = "https://router.naraya.ai";
|
|
5
|
-
|
|
6
|
-
// Known context-window overrides; default 128k/8k for everything else.
|
|
7
|
-
const CTX = { "claude-sonnet-4.5": 200000, "claude-haiku-4.5": 200000, "qwen3.7-plus-1m": 1000000 };
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Resolve the single gateway base from (in order): a `--base <url>` flag in
|
|
11
|
-
* argv, the NARAYA_BASE env var, or the production default. Returns the derived
|
|
12
|
-
* SITE (page + /api) and BASE_URL (/v1). The flag avoids cross-shell env-var
|
|
13
|
-
* syntax pain (cmd.exe vs bash vs PowerShell).
|
|
14
|
-
*/
|
|
15
|
-
export function resolveBase(argv = []) {
|
|
16
|
-
const i = argv.indexOf("--base");
|
|
17
|
-
const override = i >= 0 && argv[i + 1] ? argv[i + 1] : process.env.NARAYA_BASE;
|
|
18
|
-
const base = (override ?? DEFAULT_BASE).replace(/\/+$/, "");
|
|
19
|
-
return { SITE: base, BASE_URL: `${base}/v1` };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Back-compat constants resolved from the default/env base (no flag).
|
|
23
|
-
export const { SITE, BASE_URL } = resolveBase();
|
|
24
|
-
|
|
25
|
-
/** Write models.json for the naraya provider. 0600 — contains the API key. */
|
|
26
|
-
export function writeModelsConfig(agentDir, apiKey, modelIds, baseUrl = BASE_URL) {
|
|
27
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
28
|
-
const config = {
|
|
29
|
-
providers: {
|
|
30
|
-
naraya: {
|
|
31
|
-
name: "Naraya",
|
|
32
|
-
baseUrl,
|
|
33
|
-
api: "openai-completions",
|
|
34
|
-
apiKey,
|
|
35
|
-
models: modelIds.map((id) => ({ id, name: id, contextWindow: CTX[id] ?? 128000, maxTokens: 8192, input: ["text"] })),
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
fs.writeFileSync(path.join(agentDir, "models.json"), JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
40
|
-
}
|
|
1
|
+
import s from"node:fs";import c from"node:path";const y="https://router.naraya.ai",m=128e3,S=32e3;function A(t){try{const e=Number(JSON.parse(s.readFileSync(c.join(t,"max-output.json"),"utf8")));return Number.isFinite(e)&&e>0?Math.floor(e):null}catch{return null}}function f(t,e){if(e)return e;const o=Number(t.max_output_tokens??t.max_completion_tokens??t.maxOutputTokens);return Number.isFinite(o)&&o>0?o:S}function _(t=[]){const e=t.indexOf("--base"),r=((e>=0&&t[e+1]?t[e+1]:process.env.NARAYA_BASE)??y).replace(/\/+$/,"");return{SITE:r,BASE_URL:`${r}/v1`}}const{SITE:h,BASE_URL:w}=_();function T(t){try{const e=JSON.parse(s.readFileSync(c.join(t,"custom-models.json"),"utf8"));return Array.isArray(e)?e.map(o=>typeof o=="string"?{id:o}:o).filter(o=>o&&o.id):[]}catch{return[]}}function E(t,e,o,r=w){s.mkdirSync(t,{recursive:!0});const u=A(t),a=o.map(n=>{const i=typeof n=="string"?{id:n}:n,p=Number(i.weight),l=p>0?`${i.id} (${p}x)`:i.id;return{id:i.id,name:l,contextWindow:i.context_window??m,maxTokens:f(i,u),input:["text","image"]}}),d=new Set(a.map(n=>n.id));for(const n of T(t))d.has(n.id)||(d.add(n.id),a.push({id:n.id,name:n.name||`${n.id} (custom)`,contextWindow:n.context_window??m,maxTokens:f(n,u),input:n.vision===!1?["text"]:["text","image"]}));const x={providers:{naraya:{name:"Naraya",baseUrl:r,api:"openai-completions",apiKey:e,models:a}}};s.writeFileSync(c.join(t,"models.json"),JSON.stringify(x,null,2),{mode:384})}export{w as BASE_URL,h as SITE,T as readCustomModels,A as readMaxOutputOverride,_ as resolveBase,E as writeModelsConfig};
|
package/src/goodbye.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const c="\x1B[38;2;45;123;255m",m="\x1B[2m",s="\x1B[0m",$=/\x1b\[[0-9;?]*[a-zA-Z]/g,l=t=>t.replace($,"").length,e=["\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 ","\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551","\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551","\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"],i=Math.max(...e.map(t=>t.length));function u(){if(!process.stdout.isTTY)return;const t=process.stdout.columns||80,o=(n,r)=>" ".repeat(Math.max(0,Math.floor((t-r)/2)))+n,a=t<i+2?[o(`${c}NARAYA${s}`,6)]:e.map(n=>o(`${c}${n}${s}`,n.length)),p=o(`${m}Sampai jumpa \u{1F44B}${s}`,15);process.stdout.write(`
|
|
2
|
+
${a.join(`
|
|
3
|
+
`)}
|
|
4
|
+
|
|
5
|
+
${p}
|
|
6
|
+
|
|
7
|
+
`)}export{u as printBye};
|
package/src/login.mjs
CHANGED
|
@@ -1,49 +1,7 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
: process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
9
|
-
spawn(cmd[0], cmd[1], { stdio: "ignore", detached: true }).unref();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function login(agentDir, { SITE = DEFAULT_SITE, BASE_URL = DEFAULT_BASE_URL } = {}) {
|
|
13
|
-
const state = crypto.randomBytes(16).toString("hex");
|
|
14
|
-
const code = await new Promise((resolve, reject) => {
|
|
15
|
-
const server = http.createServer((req, res) => {
|
|
16
|
-
const u = new URL(req.url, "http://127.0.0.1");
|
|
17
|
-
if (u.pathname !== "/callback") { res.writeHead(404).end(); return; }
|
|
18
|
-
if (u.searchParams.get("state") !== state) {
|
|
19
|
-
res.writeHead(400).end("State mismatch. Close this tab and retry `naraya login`.");
|
|
20
|
-
server.close(); reject(new Error("state mismatch")); return;
|
|
21
|
-
}
|
|
22
|
-
res.writeHead(200, { "content-type": "text/html" }).end("<h3>Naraya CLI connected. You can close this tab.</h3>");
|
|
23
|
-
server.close(); resolve(u.searchParams.get("code"));
|
|
24
|
-
});
|
|
25
|
-
server.listen(0, "127.0.0.1", () => {
|
|
26
|
-
const { port } = server.address();
|
|
27
|
-
const url = `${SITE}/connect/cli?state=${state}&port=${port}`;
|
|
28
|
-
console.log(`Opening browser to sign in...\n ${url}`);
|
|
29
|
-
openBrowser(url);
|
|
30
|
-
});
|
|
31
|
-
setTimeout(() => { server.close(); reject(new Error("login timed out (5m)")); }, 300_000).unref();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const ex = await fetch(`${SITE}/api/auth/exchange`, {
|
|
35
|
-
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ code }),
|
|
36
|
-
});
|
|
37
|
-
if (!ex.ok) throw new Error(`exchange failed: HTTP ${ex.status}`);
|
|
38
|
-
const { api_key } = await ex.json();
|
|
39
|
-
if (!api_key) throw new Error("exchange response missing api_key");
|
|
40
|
-
|
|
41
|
-
const models = await fetch(`${BASE_URL}/models`, { headers: { authorization: `Bearer ${api_key}` } });
|
|
42
|
-
if (!models.ok) throw new Error(`model list failed: HTTP ${models.status}`);
|
|
43
|
-
const data = (await models.json()).data;
|
|
44
|
-
if (!Array.isArray(data)) throw new Error("model list response malformed (no data array)");
|
|
45
|
-
const ids = data.map((m) => m.id);
|
|
46
|
-
|
|
47
|
-
writeModelsConfig(agentDir, api_key, ids, BASE_URL);
|
|
48
|
-
console.log(`Signed in. ${ids.length} models configured. Run \`naraya\` to start.`);
|
|
49
|
-
}
|
|
1
|
+
import g from"node:http";import u from"node:crypto";import{spawn as p}from"node:child_process";import{writeModelsConfig as y,BASE_URL as $,SITE as E}from"./config.mjs";function S(t){if(process.platform==="win32"){p("powershell",["-NoProfile","-NonInteractive","-Command",`Start-Process '${t}'`],{stdio:"ignore",detached:!0}).unref();return}const o=process.platform==="darwin"?["open",[t]]:["xdg-open",[t]];p(o[0],o[1],{stdio:"ignore",detached:!0}).unref()}async function _(t,{SITE:o=E,BASE_URL:l=$}={}){const m=u.randomBytes(16).toString("hex"),f=await new Promise((w,h)=>{const r=g.createServer((c,e)=>{const d=new URL(c.url,"http://127.0.0.1");if(d.pathname!=="/callback"){e.writeHead(404).end();return}if(d.searchParams.get("state")!==m){e.writeHead(400).end("State mismatch. Close this tab and retry `naraya login`."),r.close(),h(new Error("state mismatch"));return}e.writeHead(200,{"content-type":"text/html"}).end("<h3>Naraya CLI connected. You can close this tab.</h3>"),r.close(),w(d.searchParams.get("code"))});r.listen(0,"127.0.0.1",()=>{const{port:c}=r.address(),e=`${o}/connect/cli?state=${m}&port=${c}`;console.log(`
|
|
2
|
+
Sign in to Naraya.
|
|
3
|
+
Trying to open your browser \u2014 if it doesn't open, copy this link in:
|
|
4
|
+
|
|
5
|
+
${e}
|
|
6
|
+
|
|
7
|
+
Waiting for you to authorize\u2026`),S(e)}),setTimeout(()=>{r.close(),h(new Error("login timed out (5m)"))},3e5).unref()}),n=await fetch(`${o}/api/auth/exchange`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({code:f})});if(!n.ok)throw new Error(`exchange failed: HTTP ${n.status}`);const{api_key:a}=await n.json();if(!a)throw new Error("exchange response missing api_key");const s=await fetch(`${l}/models`,{headers:{authorization:`Bearer ${a}`}});if(!s.ok)throw new Error(`model list failed: HTTP ${s.status}`);const i=(await s.json()).data;if(!Array.isArray(i))throw new Error("model list response malformed (no data array)");y(t,a,i,l),console.log(`Signed in. ${i.length} models configured. Run \`naraya\` to start.`)}export{_ as login};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface McpServerConfig {
|
|
5
|
+
command?: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
type?: "http" | "streamable-http";
|
|
8
|
+
url?: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface McpConfig {
|
|
15
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadMcpConfig(projectDir: string, globalDir: string): McpConfig {
|
|
19
|
+
const globalPath = path.join(globalDir, "mcp.json");
|
|
20
|
+
const projectPath = path.join(projectDir, ".mcp.json");
|
|
21
|
+
|
|
22
|
+
let globalConfig: McpConfig = { mcpServers: {} };
|
|
23
|
+
let projectConfig: McpConfig = { mcpServers: {} };
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(globalPath)) {
|
|
27
|
+
globalConfig = JSON.parse(fs.readFileSync(globalPath, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Ignore malformed global config
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(projectPath)) {
|
|
35
|
+
projectConfig = JSON.parse(fs.readFileSync(projectPath, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore malformed project config
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Merge: project wins on conflict
|
|
42
|
+
const merged: McpConfig = {
|
|
43
|
+
mcpServers: {
|
|
44
|
+
...globalConfig.mcpServers,
|
|
45
|
+
...projectConfig.mcpServers
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { McpConfig, McpServerConfig } from "./config-loader.js";
|
|
3
|
+
import { createTransport } from "./transport.js";
|
|
4
|
+
|
|
5
|
+
interface ServerConnection {
|
|
6
|
+
client: Client;
|
|
7
|
+
transport: any;
|
|
8
|
+
config: McpServerConfig;
|
|
9
|
+
tools: any[];
|
|
10
|
+
status: "connecting" | "connected" | "failed" | "disconnected";
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class McpManager {
|
|
15
|
+
private servers: Map<string, ServerConnection> = new Map();
|
|
16
|
+
private projectDir: string;
|
|
17
|
+
|
|
18
|
+
constructor(private config: McpConfig, projectDir: string) {
|
|
19
|
+
this.projectDir = projectDir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start(): Promise<void> {
|
|
23
|
+
const serverEntries = Object.entries(this.config.mcpServers ?? {});
|
|
24
|
+
|
|
25
|
+
for (const [name, serverConfig] of serverEntries) {
|
|
26
|
+
await this.startServer(name, serverConfig);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async startServer(name: string, config: McpServerConfig): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
const transport = await createTransport(config, this.projectDir);
|
|
33
|
+
const client = new Client(
|
|
34
|
+
{ name: "naraya-cli", version: "0.2.4" },
|
|
35
|
+
{ capabilities: {} }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const connection: ServerConnection = {
|
|
39
|
+
client,
|
|
40
|
+
transport,
|
|
41
|
+
config,
|
|
42
|
+
tools: [],
|
|
43
|
+
status: "connecting"
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.servers.set(name, connection);
|
|
47
|
+
|
|
48
|
+
await client.connect(transport);
|
|
49
|
+
connection.status = "connected";
|
|
50
|
+
|
|
51
|
+
// Discover tools
|
|
52
|
+
const toolsResult = await client.listTools();
|
|
53
|
+
connection.tools = toolsResult.tools ?? [];
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
const connection = this.servers.get(name);
|
|
56
|
+
if (connection) {
|
|
57
|
+
connection.status = "failed";
|
|
58
|
+
connection.error = err.message;
|
|
59
|
+
}
|
|
60
|
+
console.error(`[MCP] Failed to start server '${name}': ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async stop(): Promise<void> {
|
|
65
|
+
for (const [name, conn] of this.servers.entries()) {
|
|
66
|
+
try {
|
|
67
|
+
if (conn.status === "connected") {
|
|
68
|
+
await conn.client.close();
|
|
69
|
+
}
|
|
70
|
+
conn.status = "disconnected";
|
|
71
|
+
} catch (err: any) {
|
|
72
|
+
console.error(`[MCP] Error stopping server '${name}': ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.servers.clear();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getServerStatus(): Record<string, { status: string; toolCount: number; error?: string }> {
|
|
79
|
+
const result: Record<string, any> = {};
|
|
80
|
+
for (const [name, conn] of this.servers.entries()) {
|
|
81
|
+
result[name] = {
|
|
82
|
+
status: conn.status,
|
|
83
|
+
toolCount: conn.tools.length,
|
|
84
|
+
error: conn.error
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async callTool(serverName: string, toolName: string, params: any): Promise<any> {
|
|
91
|
+
const conn = this.servers.get(serverName);
|
|
92
|
+
if (!conn) {
|
|
93
|
+
throw new Error(`MCP server '${serverName}' not found`);
|
|
94
|
+
}
|
|
95
|
+
if (conn.status !== "connected") {
|
|
96
|
+
throw new Error(`MCP server '${serverName}' is not connected (status: ${conn.status})`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return await conn.client.callTool({ name: toolName, arguments: params });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getTools(): Array<{ serverName: string; tool: any }> {
|
|
103
|
+
const result: Array<{ serverName: string; tool: any }> = [];
|
|
104
|
+
for (const [serverName, conn] of this.servers.entries()) {
|
|
105
|
+
if (conn.status === "connected") {
|
|
106
|
+
for (const tool of conn.tools) {
|
|
107
|
+
result.push({ serverName, tool });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface PiToolConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description: string;
|
|
5
|
+
parameters: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function mapMcpToolToPi(
|
|
9
|
+
serverName: string,
|
|
10
|
+
mcpTool: any
|
|
11
|
+
): PiToolConfig {
|
|
12
|
+
const toolName = `mcp__${serverName}__${mcpTool.name}`;
|
|
13
|
+
const label = `${serverName}: ${mcpTool.name}`;
|
|
14
|
+
const description = mcpTool.description ?? `MCP tool: ${mcpTool.name}`;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
name: toolName,
|
|
18
|
+
label,
|
|
19
|
+
description,
|
|
20
|
+
parameters: mcpTool.inputSchema ?? { type: "object", properties: {} }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mapMcpResultToPi(result: any): any {
|
|
25
|
+
const content = result.content ?? [];
|
|
26
|
+
return {
|
|
27
|
+
content: content.map((c: any) => {
|
|
28
|
+
if (c.type === "text") return { type: "text", text: c.text };
|
|
29
|
+
if (c.type === "image") return { type: "image", data: c.data, mimeType: c.mimeType };
|
|
30
|
+
return { type: "text", text: JSON.stringify(c) };
|
|
31
|
+
}),
|
|
32
|
+
details: {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function mapMcpErrorToPi(err: any): any {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: `MCP error: ${err.message}` }],
|
|
39
|
+
isError: true,
|
|
40
|
+
details: {}
|
|
41
|
+
};
|
|
42
|
+
}
|