@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
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Naraya CLI — Proprietary License
|
|
2
|
+
Copyright (c) 2026 PT. Naraya Teknologi Indonesia. All rights reserved.
|
|
3
|
+
|
|
4
|
+
This software and its accompanying assets (including but not limited to system
|
|
5
|
+
prompts, agent definitions, skills, and configuration) are the confidential and
|
|
6
|
+
proprietary property of PT. Naraya Teknologi Indonesia ("Naraya").
|
|
7
|
+
|
|
8
|
+
NO LICENSE IS GRANTED to copy, modify, distribute, sublicense, reverse engineer,
|
|
9
|
+
decompile, disassemble, or create derivative works from any part of this
|
|
10
|
+
software or its assets, in whole or in part, except as expressly authorized in
|
|
11
|
+
writing by Naraya.
|
|
12
|
+
|
|
13
|
+
Use of this software requires a valid Naraya account and API key and is governed
|
|
14
|
+
by the Naraya Terms of Service. Access may be revoked at any time.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL NARAYA BE LIABLE
|
|
19
|
+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,93 +1,184 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
## Usage
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
1
|
+
# Naraya CLI
|
|
2
|
+
|
|
3
|
+
> One sign-in, every model. A coding agent that delegates specialist work to focused subagents and brings evidence to every claim.
|
|
4
|
+
|
|
5
|
+
[](https://nodejs.org) [](LICENSE) [](package.json)
|
|
6
|
+
|
|
7
|
+
Naraya CLI is an AI coding agent that:
|
|
8
|
+
|
|
9
|
+
- Routes all model traffic through [router.naraya.ai](https://router.naraya.ai) with one API key — Claude, GPT, Gemini, DeepSeek, GLM, Kimi, MiniMax, Mistral, and more.
|
|
10
|
+
- Ships a roster of focused subagents (`nara-build`, `nara-architect`, `nara-release`, `nara-review`, `nara-debug`, `nara-plan`, `nara-explore`, `nara-search`, `nara-fe`, `nara-droid`) — each with its own skills and references.
|
|
11
|
+
- Provides a `delegate` tool that fans work out to subagents in parallel, sequential chains, or single focus — each in an isolated context window.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Table of contents
|
|
16
|
+
|
|
17
|
+
- [Install](#install)
|
|
18
|
+
- [Quickstart](#quickstart)
|
|
19
|
+
- [Usage](#usage)
|
|
20
|
+
- [Subagents](#subagents)
|
|
21
|
+
- [Skills](#skills)
|
|
22
|
+
- [Support](#support)
|
|
23
|
+
- [License](#license)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Requires Node.js >= 22.19
|
|
31
|
+
npm install -g @naraya/cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then sign in with your Naraya account:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
naraya login
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> Naraya CLI is proprietary software. Access requires a valid Naraya account and
|
|
41
|
+
> API key — sign up at [router.naraya.ai](https://router.naraya.ai).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# 1. Sign in (one-time)
|
|
49
|
+
naraya login
|
|
50
|
+
|
|
51
|
+
# 2. Verify auth
|
|
52
|
+
naraya status
|
|
53
|
+
|
|
54
|
+
# 3. Launch the agent in the current project
|
|
55
|
+
naraya
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Inside the agent, press `/` for the command palette, `/agents` to list subagents, `/skills` to list skills.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Quick subcommands without launching the TUI
|
|
62
|
+
naraya login # sign in to router.naraya.ai
|
|
63
|
+
naraya logout # wipe local credentials
|
|
64
|
+
naraya status # show auth + quota + current model
|
|
65
|
+
naraya status --usd # same, with USD cost equivalent
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
### Interactive mode
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
naraya # launch in cwd
|
|
76
|
+
naraya --model naraya/glm-5 # override default model for this session
|
|
77
|
+
naraya --continue # resume the last session
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Slash commands inside the TUI:
|
|
81
|
+
|
|
82
|
+
| Command | Effect |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `/agents` | list subagents |
|
|
85
|
+
| `/agent-model <name>` | change a subagent's model |
|
|
86
|
+
| `/skill:<name> [args]` | load a skill |
|
|
87
|
+
| `/skills` | list available skills |
|
|
88
|
+
| `/help` | full command palette |
|
|
89
|
+
|
|
90
|
+
### Headless / scripted
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
naraya -p "audit the auth module for OWASP top 10" # single prompt, prints result, exits
|
|
94
|
+
naraya -p --json <task> # machine-readable output
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Delegation patterns
|
|
98
|
+
|
|
99
|
+
The agent delegates to specialists via the `delegate` tool. You do not invoke it directly — you phrase the request and the model decides.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// One focused specialist
|
|
103
|
+
delegate { agent: "nara-droid", task: "..." }
|
|
104
|
+
|
|
105
|
+
// Independent units in parallel
|
|
106
|
+
delegate { tasks: [
|
|
107
|
+
{ agent: "nara-search", task: "research X" },
|
|
108
|
+
{ agent: "nara-explore", task: "find usages of Y" },
|
|
109
|
+
{ agent: "nara-plan", task: "draft plan for Z" }
|
|
110
|
+
]}
|
|
111
|
+
|
|
112
|
+
// Sequential, {previous} injects the prior step's output
|
|
113
|
+
delegate { chain: [
|
|
114
|
+
{ agent: "nara-explore", task: "map the auth flow" },
|
|
115
|
+
{ agent: "nara-plan", task: "draft a plan based on {previous}" },
|
|
116
|
+
{ agent: "nara-review", task: "review the plan in {previous}" }
|
|
117
|
+
]}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Subagents
|
|
123
|
+
|
|
124
|
+
Naraya ships a roster of focused subagents. The model picks based on the **unit of work**, not the surface form.
|
|
125
|
+
|
|
126
|
+
### Coordination
|
|
127
|
+
|
|
128
|
+
| Agent | Purpose | When to use |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `nara-build` | Implements features, bugfixes, refactors, releases | Top-level orchestrator. Default for "implement X", "fix Y", "refactor Z" |
|
|
131
|
+
| `nara-architect` | System design, ADRs, component decomposition | Greenfield design, scaling decisions, API contracts, trade-offs. Read-only |
|
|
132
|
+
| `nara-release` | Version sync, verification, deploy safety | Before any production push, store release, or schema migration |
|
|
133
|
+
| `nara-review` | Multi-aspect code review (security, perf, a11y, API, tests) | Pre-merge review, audit, self-review. Does not fix |
|
|
134
|
+
| `nara-debug` | 4-phase root-cause debugging | "X is broken", flaky tests, intermittent failures |
|
|
135
|
+
|
|
136
|
+
### Investigation (read-only)
|
|
137
|
+
|
|
138
|
+
| Agent | Purpose | When to use |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `nara-plan` | Implementation plans, never edits code | Pair with `nara-build` for hand-off |
|
|
141
|
+
| `nara-explore` | Fast codebase navigation | "Where is X defined?", "who calls Y?", quick map |
|
|
142
|
+
| `nara-search` | Evidence-first research with citations | Library comparison, version migration, doc lookup |
|
|
143
|
+
|
|
144
|
+
### Specialist (writes code in their domain)
|
|
145
|
+
|
|
146
|
+
| Agent | Purpose | When to use |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `nara-fe` | Frontend UI/UX (React, Vue, Svelte, Angular, CSS, Tailwind, a11y) | UI changes, component design, responsive, accessibility |
|
|
149
|
+
| `nara-droid` | Native Android (Kotlin, Gradle, Compose, Room, Hilt, adb, APK/AAB) | Android tasks, build failures, release |
|
|
150
|
+
|
|
151
|
+
If multiple roles apply, the model fans out to parallel subagents.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Skills
|
|
156
|
+
|
|
157
|
+
Naraya bundles a large pool of skills that any agent can load on demand via `/skill:<name>`. Highlights:
|
|
158
|
+
|
|
159
|
+
| Skill | What it covers |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `systematic-debugging` | 4-phase root-cause debugging |
|
|
162
|
+
| `test-driven-development` | RED-GREEN-REFACTOR discipline |
|
|
163
|
+
| `writing-plans` | Implementation plans with bite-sized tasks |
|
|
164
|
+
| `security`, `auth-identity`, `compliance-governance` | Security review and compliance |
|
|
165
|
+
| `architecture`, `api-design-patterns`, `distributed-systems` | System design |
|
|
166
|
+
| `android-kotlin`, `compose-patterns`, `gradle-troubleshoot` | Android development |
|
|
167
|
+
| `react-patterns`, `css-patterns` | Frontend |
|
|
168
|
+
|
|
169
|
+
**Skills vs subagents:** a skill is a workflow / lens the agent applies inline. A subagent is a separate context that runs in parallel. Use skills for "do X the right way", subagents for "do X in isolation".
|
|
170
|
+
|
|
171
|
+
For project-specific guidance, add an `AGENTS.md` to your repository — the agent reads it automatically.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Support
|
|
176
|
+
|
|
177
|
+
- **Account, billing, API keys:** [router.naraya.ai](https://router.naraya.ai)
|
|
178
|
+
- **Sign-in issues:** run `naraya logout` then `naraya login` to refresh your session.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
Proprietary. Copyright © 2026 PT. Naraya Teknologi Indonesia. All rights reserved. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import D from"node:os";import p from"node:path";import f from"node:fs";import"node:crypto";import{AuthStorage as te,ModelRegistry as ne,SessionManager as se,createAgentSession as ae,initTheme as q,getMarkdownTheme as re,getSelectListTheme as oe}from"@earendil-works/pi-coding-agent";import{fileURLToPath as ie}from"node:url";import{TUI as ce,Container as b,Editor as Be,Text as x,Markdown as _e,ProcessTerminal as le,KeybindingsManager as de,TUI_KEYBINDINGS as me,setKeybindings as $e}from"@earendil-works/pi-tui";import{readClipboardImage as ue}from"../src/clipboard.mjs";import"../src/splash.mjs";const F=p.join(D.homedir(),".naraya","native-debug.log"),$=e=>{try{f.appendFileSync(F,`${new Date().toISOString()} ${e}
|
|
3
|
+
`)}catch{}};try{f.rmSync(F,{force:!0})}catch{}process.on("uncaughtException",e=>{$(`UNCAUGHT ${e?.stack??e}`)}),process.on("unhandledRejection",e=>{$(`UNHANDLED ${e?.stack??e}`)});const c="\x1B[38;2;45;123;255m",pe="\x1B[48;2;45;123;255m",he="\x1B[38;2;10;29;77m",ge="\x1B[48;2;10;29;77m",T="\x1B[38;2;255;140;40m",Ne="\x1B[38;2;111;191;115m",B="\x1B[2m",k="\x1B[1m",s="\x1B[0m",fe=/\x1b\[[0-9;]*m/g,m=e=>e.replace(fe,"").length,Ue=(()=>{try{const e=p.dirname(p.dirname(ie(import.meta.url)));return JSON.parse(f.readFileSync(p.join(e,"package.json"),"utf8")).version??"dev"}catch{return"dev"}})(),w=["_____________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=e=>e==="B"?c:e==="N"?he:"",ye=e=>e==="B"?pe:e==="N"?ge:"";function Ye(){const e=[];for(let t=0;t<w.length;t+=2){let n="";for(let r=0;r<w[t].length;r++){const a=w[t][r],o=w[t+1]?.[r]??"_";a==="_"&&o==="_"?n+=" ":a!=="_"&&o==="_"?n+=`${M(a)}\u2580${s}`:a==="_"&&o!=="_"?n+=`${M(o)}\u2584${s}`:a===o?n+=`${M(a)}\u2588${s}`:n+=`${M(a)}${ye(o)}\u2580${s}`}e.push(n)}return e}const j=(e,t)=>e+" ".repeat(Math.max(0,t-m(e)));function Ke(e,t){const n=Math.max(...e.map(m),0),r=Math.max(...t.map(m),0),a=Math.max(e.length,t.length),o=Math.floor((a-e.length)/2),_=Math.floor((a-t.length)/2);return Array.from({length:a},(N,l)=>`${j(e[l-o]??"",n)} ${j(t[l-_]??"",r)}`)}const E=p.join(D.homedir(),".naraya","agent"),G=p.join(E,"models.json");let U;try{U=JSON.parse(f.readFileSync(G,"utf8"))}catch{console.error("Run `naraya login` first."),process.exit(1)}const y=U.providers?.naraya??{},be=(y.models??[]).map(e=>e.id),h=process.env.NARAYA_MODEL??be[0],xe=(y.models??[]).find(e=>e.id===h)?.name??h;h||(console.error("No naraya models \u2014 run `naraya login`."),process.exit(1));const Y=te.create(p.join(E,"auth.json")),K=ne.create(Y,G),P=K.find("naraya",h);P||(console.error(`Model naraya/${h} not found.`),process.exit(1));const{session:I}=await ae({cwd:process.cwd(),agentDir:E,model:P,thinkingLevel:"off",authStorage:Y,modelRegistry:K,tools:["read","grep","find","ls","edit","write","bash"],sessionManager:se.create(process.cwd())});try{$e(new de(me))}catch{}try{q("naraya",!0)}catch{try{q("dark",!0)}catch{}}const we=re(),Me={borderColor:e=>`${c}${e}${s}`,selectList:oe()};let V=!1,J=!1,z=0,H=0,u=null;async function Se(){try{const e=await fetch(`${y.baseUrl}/me`,{headers:{authorization:`Bearer ${y.apiKey}`},signal:AbortSignal.timeout(3e3)});if(!e.ok)return;const t=await e.json(),n=Number(t.quota?.limit??0),r=Number(t.quota?.remaining??0),a=o=>o>=1e6?`${(o/1e6).toFixed(1).replace(/\.0$/,"")}M`:Number(o).toLocaleString("id-ID");u={email:t.account?.email??"",plan:t.account?.plan??"",saldo:`Rp ${Number(t.credit?.available??0).toLocaleString("id-ID",{maximumFractionDigits:0})}`,usd:t.credit?.usd_equivalent?`$${t.credit.usd_equivalent}`:"",kuota:n>0?`${a(r)} / ${a(n)}`:"fair-use",models:Array.isArray(t.models)?t.models.length:0}}catch{}}const Ae={render(e){const t=Math.min(48,Math.max(40,e-2)),n=t-4,r=(d,L)=>{const ee=Math.max(1,n-m(d)-m(L));return`${d}${" ".repeat(ee)}${L}`},a=y?`${B} v${process.env.NARAYA_VERSION??"dev"}${s}${c}`:"",o=` ${k}NARAYA${s}${a}${c} `,_=`${c}\u256D\u2500${o}${"\u2500".repeat(Math.max(0,t-m(o)-3))}\u256E${s}`,N=`${c}\u2570${"\u2500".repeat(Math.max(0,t-2))}\u256F${s}`,l=d=>`${c}\u2502${s} ${d}${" ".repeat(Math.max(0,n-m(d)))} ${c}\u2502${s}`;return u?[_,l(r(`${k}${u.email}${s}`,`${B}${u.plan}${s}`)),l(r(`${c}Saldo${s} ${u.saldo}`,`${B}${u.usd}${s}`)),l(r(`${c}Kuota${s} ${u.kuota}`,`${B}${u.models} model${s}`)),N,""]:[_,l(`${B}memuat akun\u2026${s}`),N,""]}},Ce={render(e){const t=process.cwd(),n=`${c}\u25C6 ${xe}${s}`,r=Math.max(1,e-m(t)-m(n)),a=`${B}${t}${s}${" ".repeat(r)}${n}`,o=d=>d<1e3?`${d}`:`${(d/1e3).toFixed(1).replace(/\.0$/,"")}k`,_=`${B}\u2191${o(z)} \u2193${o(H)} tokens${s}`,N=`${V?T:B}\u26A1 auto-approve ${V?"ON":"OFF"} \xB7 Ctrl+Alt+A${s}`,l=`${J?T:B}\u2702\uFE0F truncate ${J?"ON":"OFF"} \xB7 Ctrl+Alt+O${s}`;return["\u2500".repeat(e),a,_,N,l]}},i=new ce(new le,!1),W=()=>{try{i.stop()}catch{}try{I.dispose?.()}catch{}process.exit(0)};process.on("SIGINT",W);const X=new b,g=new b,Q=new b,Z=new b;X.addChild(Ae),Z.addChild(Ce);let R=null,O="",S=!1;const A=new Map;let C=0;async function Te(e){$(`submit text=${JSON.stringify((e??"").slice(0,40))} busy=${S}`);let t=(e??"").trim();if(!t&&A.size===0||S){$("submit skipped (empty/busy)");return}const n=[];t=t.replace(/\[🖼 Image #(\d+)\]/g,(a,o)=>{const _=A.get(Number(o));if(_)try{n.push({type:"image",data:f.readFileSync(_.path).toString("base64"),mimeType:_.mimeType})}catch{}return""}).replace(/\s{2,}/g," ").trim(),A.clear(),C=0;const r=n.length?`${B}\u{1F4CE} ${n.length} gambar${s} `:"";g.addChild(new x(`${r}${k}\u203A${s} ${t||"(gambar)"}`,1,0)),O="",R=new _e("",1,0,we),g.addChild(R),i.requestRender(),S=!0;try{await I.prompt(t||"Describe this image.",n.length?{images:n}:void 0)}catch(a){$(`prompt error ${a?.stack??a}`),g.addChild(new x(`${T}[error]${s} ${a?.message??a}`,1,0))}finally{S=!1,i.requestRender()}}class ke extends Be{handleInput(t){if($(`handleInput ${JSON.stringify(t.slice(0,10))} len=${t.length}`),t===""){W();return}if(t==="\r"||t===`
|
|
4
|
+
`){const n=this.getText();this.setText(""),Te(n);return}if(t==="\x1Bv"||t==="\x1BV"){const n=ue();if(n){C+=1,A.set(C,n);const r=this.getText(),a=r&&!r.endsWith(" ")?" ":"";this.setText(`${r}${a}[\u{1F5BC} Image #${C}] `),i.requestRender()}return}super.handleInput(t)}}const v=new ke(i,Me,{placeholder:"Pesan ke Naraya \u2014 Enter kirim \xB7 Alt+V tempel gambar \xB7 Ctrl+C keluar"});Q.addChild(v),I.subscribe(e=>{if($(`event ${e.type}${e.assistantMessageEvent?.type?"/"+e.assistantMessageEvent.type:""}`),e.type==="message_update"&&e.assistantMessageEvent?.type==="text_delta")O+=e.assistantMessageEvent.delta,R?.setText(O),i.requestRender();else if(e.type==="tool_execution_start")g.addChild(new x(`${B}\u2699 ${e.toolName??"tool"}\u2026${s}`,1,0)),i.requestRender();else if(e.type==="turn_end"){const t=e.message?.usage;t&&(z+=t.input??0,H+=t.output??0),i.requestRender()}}),i.addChild(X),i.addChild(g),i.addChild(Q),i.addChild(Z),i.setFocus(v),i.start(),$(`started; focus=${i.focusedComponent===v?"editor":"other"} isTTY=${process.stdin.isTTY}`),g.addChild(new x(`${Ne}\u2713${s} Naraya native \u2014 naraya/${h}. Ketik pesan, Enter kirim.`,1,0)),Se().then(()=>i.requestRender());
|
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
|
+
}
|