@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.
Files changed (83) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +184 -93
  3. package/bin/naraya-native.mjs +4 -0
  4. package/bin/naraya.mjs +1 -142
  5. package/bin/undici-timeout.mjs +1 -0
  6. package/dist/assets.pack.gz +0 -0
  7. package/dist/mcp/config-loader.js +32 -0
  8. package/dist/mcp/lifecycle.js +90 -0
  9. package/dist/mcp/tool-mapper.js +31 -0
  10. package/dist/mcp/transport.js +30 -0
  11. package/dist/pentest/catalog/catalog-loader.js +45 -0
  12. package/dist/pentest/catalog/index.js +1 -0
  13. package/dist/pentest/cli.js +117 -0
  14. package/dist/pentest/command-builder/command-builder.js +90 -0
  15. package/dist/pentest/command-builder/index.js +1 -0
  16. package/dist/pentest/index.js +10 -0
  17. package/dist/pentest/installer/index.js +1 -0
  18. package/dist/pentest/installer/tool-installer.js +90 -0
  19. package/dist/pentest/manager.js +125 -0
  20. package/dist/pentest/mode/index.js +1 -0
  21. package/dist/pentest/mode/mode-selector.js +127 -0
  22. package/dist/pentest/selector/index.js +1 -0
  23. package/dist/pentest/selector/tool-selector.js +66 -0
  24. package/dist/pentest/skill-bridge/index.js +1 -0
  25. package/dist/pentest/skill-bridge/skill-bridge.js +66 -0
  26. package/dist/pentest/skills/generator/index.js +1 -0
  27. package/dist/pentest/skills/generator/skill-generator.js +310 -0
  28. package/dist/pentest/skills/index.js +3 -0
  29. package/dist/pentest/skills/loader/index.js +1 -0
  30. package/dist/pentest/skills/loader/skill-loader.js +167 -0
  31. package/dist/pentest/skills/register/index.js +1 -0
  32. package/dist/pentest/skills/register/skill-register.js +162 -0
  33. package/dist/pentest/skills/types.js +1 -0
  34. package/dist/pentest/types.js +90 -0
  35. package/package.json +42 -14
  36. package/src/assets-pack.mjs +1 -0
  37. package/src/banner.mjs +5 -0
  38. package/src/clipboard.mjs +1 -0
  39. package/src/config.mjs +1 -40
  40. package/src/goodbye.mjs +7 -0
  41. package/src/login.mjs +7 -49
  42. package/src/mcp/config-loader.ts +50 -0
  43. package/src/mcp/lifecycle.ts +113 -0
  44. package/src/mcp/tool-mapper.ts +42 -0
  45. package/src/mcp/transport.ts +38 -0
  46. package/src/mcp-cli.mjs +5 -0
  47. package/src/pentest/catalog/catalog-loader.ts +55 -0
  48. package/src/pentest/catalog/index.ts +1 -0
  49. package/src/pentest/cli.ts +130 -0
  50. package/src/pentest/command-builder/command-builder.ts +109 -0
  51. package/src/pentest/command-builder/index.ts +1 -0
  52. package/src/pentest/index.ts +11 -0
  53. package/src/pentest/installer/index.ts +1 -0
  54. package/src/pentest/installer/tool-installer.ts +107 -0
  55. package/src/pentest/manager.ts +167 -0
  56. package/src/pentest/mode/index.ts +1 -0
  57. package/src/pentest/mode/mode-selector.ts +159 -0
  58. package/src/pentest/selector/index.ts +1 -0
  59. package/src/pentest/selector/tool-selector.ts +87 -0
  60. package/src/pentest/skill-bridge/index.ts +1 -0
  61. package/src/pentest/skill-bridge/skill-bridge.ts +86 -0
  62. package/src/pentest/skills/generator/index.ts +1 -0
  63. package/src/pentest/skills/generator/skill-generator.ts +373 -0
  64. package/src/pentest/skills/index.ts +4 -0
  65. package/src/pentest/skills/loader/index.ts +1 -0
  66. package/src/pentest/skills/loader/skill-loader.ts +206 -0
  67. package/src/pentest/skills/register/index.ts +1 -0
  68. package/src/pentest/skills/register/skill-register.ts +196 -0
  69. package/src/pentest/skills/types.ts +66 -0
  70. package/src/pentest/types.ts +341 -0
  71. package/src/seed.mjs +1 -36
  72. package/src/splash.mjs +4 -0
  73. package/src/status.mjs +2 -71
  74. package/assets/APPEND-SYSTEM.md +0 -9
  75. package/assets/extensions/naraya-brand.ts +0 -251
  76. package/assets/extensions/naraya-gate.ts +0 -23
  77. package/assets/naraya-logo.txt +0 -5
  78. package/assets/skills/narabuild/SKILL.md +0 -156
  79. package/assets/skills/naradroid/SKILL.md +0 -118
  80. package/assets/skills/naraexplore/SKILL.md +0 -71
  81. package/assets/skills/narafe/SKILL.md +0 -94
  82. package/assets/skills/naraplan/SKILL.md +0 -47
  83. package/assets/skills/narasearch/SKILL.md +0 -141
package/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
- # NaraCLI
2
-
3
-
4
-
5
- ## Getting started
6
-
7
- To make it easy for you to get started with GitLab, here's a list of recommended next steps.
8
-
9
- Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
10
-
11
- ## Add your files
12
-
13
- * [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files
14
- * [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
15
-
16
- ```
17
- cd existing_repo
18
- git remote add origin https://gitlab.naraya.ai/adearman/naracli.git
19
- git branch -M main
20
- git push -uf origin main
21
- ```
22
-
23
- ## Integrate with your tools
24
-
25
- * [Set up project integrations](https://gitlab.naraya.ai/adearman/naracli/-/settings/integrations)
26
-
27
- ## Collaborate with your team
28
-
29
- * [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/)
30
- * [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/)
31
- * [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically)
32
- * [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/)
33
- * [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
34
-
35
- ## Test and Deploy
36
-
37
- Use the built-in continuous integration in GitLab.
38
-
39
- * [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/)
40
- * [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/)
41
- * [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/)
42
- * [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/)
43
- * [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
44
-
45
- ***
46
-
47
- # Editing this README
48
-
49
- When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
50
-
51
- ## Suggestions for a good README
52
-
53
- Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
54
-
55
- ## Name
56
- Choose a self-explaining name for your project.
57
-
58
- ## Description
59
- Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
60
-
61
- ## Badges
62
- On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
63
-
64
- ## Visuals
65
- Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
66
-
67
- ## Installation
68
- Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
69
-
70
- ## Usage
71
- Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
72
-
73
- ## Support
74
- Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
75
-
76
- ## Roadmap
77
- If you have ideas for releases in the future, it is a good idea to list them in the README.
78
-
79
- ## Contributing
80
- State if you are open to contributions and what your requirements are for accepting them.
81
-
82
- For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
83
-
84
- You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
85
-
86
- ## Authors and acknowledgment
87
- Show your appreciation to those who have contributed to the project.
88
-
89
- ## License
90
- For open source projects, say how it is licensed.
91
-
92
- ## Project status
93
- If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
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
+ [![Node](https://img.shields.io/badge/node-%E2%89%A522.19-blue)](https://nodejs.org) [![License: Proprietary](https://img.shields.io/badge/license-proprietary-red)](LICENSE) [![Version](https://img.shields.io/badge/version-0.4.0-blue)](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 { spawn } from "node:child_process";
3
- import { fileURLToPath } from "node:url";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import os from "node:os";
7
- import { seed } from "../src/seed.mjs";
8
-
9
- const NARAYA_DIR = path.join(os.homedir(), ".naraya", "agent");
10
-
11
- // Subcommand dispatch — handled before pi resolution so these never depend on
12
- // the agent binary or an existing models.json.
13
- const sub = process.argv[2];
14
- if (sub === "login" || sub === "logout" || sub === "status") {
15
- // `--base <url>` (or NARAYA_BASE) selects the gateway, robust across shells.
16
- const { resolveBase } = await import("../src/config.mjs");
17
- const base = resolveBase(process.argv);
18
- if (sub === "login") {
19
- const { login } = await import("../src/login.mjs");
20
- await login(NARAYA_DIR, base);
21
- } else if (sub === "logout") {
22
- fs.rmSync(path.join(NARAYA_DIR, "models.json"), { force: true });
23
- console.log("Signed out. Revoke the 'Naraya CLI' key in your dashboard.");
24
- } else {
25
- const { status } = await import("../src/status.mjs");
26
- await status(NARAYA_DIR, base, { usd: process.argv.includes("--usd") });
27
- }
28
- process.exit(0);
29
- }
30
-
31
- // Resolve pi's bin from our own pinned dependency, never from PATH. The package
32
- // is ESM-only (exports has no "require" condition), so use import.meta.resolve
33
- // to find its main entry, walk up to the package root, then read bin from disk.
34
- function piPackageRoot() {
35
- let dir = path.dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent")));
36
- while (!fs.existsSync(path.join(dir, "package.json"))) {
37
- const up = path.dirname(dir);
38
- if (up === dir) throw new Error("could not locate @earendil-works/pi-coding-agent package root");
39
- dir = up;
40
- }
41
- return dir;
42
- }
43
- const piRoot = piPackageRoot();
44
- const piPkgPath = path.join(piRoot, "package.json");
45
- const piPkg = JSON.parse(fs.readFileSync(piPkgPath, "utf8"));
46
- const piBinField = piPkg.bin;
47
- const piBinRel = typeof piBinField === "string" ? piBinField : piBinField.pi;
48
- const piBin = path.join(piRoot, piBinRel);
49
-
50
- // White-label the engine: pi reads `piConfig.name` from its own package.json and
51
- // uses it as the app name + window title (otherwise "pi"/"π"). Set it to Naraya
52
- // so the terminal title is "Naraya - …" instead of "π - …". This is pi's own
53
- // white-label mechanism; the patch is idempotent. Best-effort: a read-only
54
- // install just keeps the default title.
55
- const APP = "Naraya";
56
- function ensureRebrand() {
57
- try {
58
- if (piPkg.piConfig?.name === APP) return;
59
- piPkg.piConfig = { ...(piPkg.piConfig ?? {}), name: APP };
60
- fs.writeFileSync(piPkgPath, JSON.stringify(piPkg, null, 2));
61
- } catch {
62
- /* read-only dependency — title stays default, everything else still works */
63
- }
64
- }
65
- ensureRebrand();
66
-
67
- // Seed bundled skills + extensions into the agent config dir (managed files only).
68
- const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
69
- seed(path.join(pkgRoot, "assets"), NARAYA_DIR);
70
-
71
- // Default to a quiet startup so pi's own header ("pi v0.79.1", resources list)
72
- // is suppressed and only the Naraya banner shows. Merge (don't clobber) the
73
- // user's settings; leave it alone if they've set it explicitly.
74
- function ensureQuietStartup() {
75
- const p = path.join(NARAYA_DIR, "settings.json");
76
- let s = {};
77
- try { s = JSON.parse(fs.readFileSync(p, "utf8")); } catch { /* none yet */ }
78
- if (s.quietStartup === undefined) {
79
- s.quietStartup = true;
80
- fs.mkdirSync(NARAYA_DIR, { recursive: true });
81
- fs.writeFileSync(p, JSON.stringify(s, null, 2));
82
- }
83
- }
84
- ensureQuietStartup();
85
-
86
- // Brand the system prompt so the model reaches for the bundled Nara skills.
87
- const appendSystem = path.join(pkgRoot, "assets", "APPEND-SYSTEM.md");
88
-
89
- // Default to the Naraya provider and restrict Ctrl+P model cycling to Naraya
90
- // models, so the experience is Naraya-only unless the user explicitly overrides.
91
- // Guarded on a configured `naraya` provider so we never inject an
92
- // unknown-provider error before `naraya login` has written models.json.
93
- function narayaDefaults(argv) {
94
- let hasNaraya = false;
95
- try {
96
- const cfg = JSON.parse(fs.readFileSync(path.join(NARAYA_DIR, "models.json"), "utf8"));
97
- hasNaraya = Boolean(cfg.providers?.naraya);
98
- } catch {
99
- /* no models.json yet — let pi handle provider resolution */
100
- }
101
- if (!hasNaraya) return [];
102
- const extra = [];
103
- if (!argv.includes("--provider")) extra.push("--provider", "naraya");
104
- if (!argv.includes("--models")) extra.push("--models", "naraya/*");
105
- return extra;
106
- }
107
-
108
- const userArgs = process.argv.slice(2);
109
-
110
- // Require sign-in before launching the agent. Skip the gate for informational
111
- // flags that work without a provider (version/help/model listing).
112
- const INFO_FLAGS = ["--version", "-v", "--help", "-h", "--list-models", "--models"];
113
- const isInfoOnly = userArgs.some((a) => INFO_FLAGS.includes(a));
114
- if (!isInfoOnly && !fs.existsSync(path.join(NARAYA_DIR, "models.json"))) {
115
- console.error("Run `naraya login` first.");
116
- process.exit(1);
117
- }
118
-
119
- // Show ONLY the bundled Nara skills: pi otherwise discovers skills from the cwd,
120
- // any ancestor `.agents/skills`, and global dirs, leaking unrelated skills into
121
- // the picker. Disable all discovery, then load just our seeded skills dir (which
122
- // also picks up anything the user drops in ~/.naraya/agent/skills).
123
- const skillArgs = userArgs.includes("--no-skills")
124
- ? []
125
- : ["--no-skills", "--skill", path.join(NARAYA_DIR, "skills")];
126
-
127
- const piArgs = [piBin, "--append-system-prompt", appendSystem, ...skillArgs, ...narayaDefaults(userArgs), ...userArgs];
128
-
129
- const child = spawn(process.execPath, piArgs, {
130
- stdio: "inherit",
131
- // PI_OFFLINE silences the "pi update available / pi.dev" startup banner (a
132
- // startup network op); model calls happen at runtime and are unaffected.
133
- // Set BOTH agent-dir env names: pi reads `<APP_NAME>_CODING_AGENT_DIR`, which
134
- // becomes NARAYA_CODING_AGENT_DIR once the rebrand patch applies, but stays
135
- // PI_CODING_AGENT_DIR if the patch couldn't write — so set both.
136
- env: {
137
- ...process.env,
138
- PI_CODING_AGENT_DIR: NARAYA_DIR,
139
- NARAYA_CODING_AGENT_DIR: NARAYA_DIR,
140
- PI_OFFLINE: "1",
141
- },
142
- });
143
- child.on("exit", (code) => process.exit(code ?? 0));
2
+ import{spawn as A}from"node:child_process";import{fileURLToPath as u,pathToFileURL as k}from"node:url";import r from"node:fs";import i from"node:path";import v from"node:os";import{seed as N,seedEntries as R}from"../src/seed.mjs";const o=i.join(v.homedir(),".naraya","agent"),n=process.argv[2];if(n==="login"||n==="logout"||n==="status"||n==="pentest"||n==="mcp"){const{resolveBase:e}=await import("../src/config.mjs"),s=e(process.argv);if(n==="login"){const{login:t}=await import("../src/login.mjs");await t(o,s)}else if(n==="logout")r.rmSync(i.join(o,"models.json"),{force:!0}),console.log("Signed out. Revoke the 'Naraya CLI' key in your dashboard.");else if(n==="status"){const{status:t}=await import("../src/status.mjs");await t(o,s,{usd:process.argv.includes("--usd")})}else if(n==="pentest"){const{pentestCLI:t}=await import("../dist/pentest/cli.js");await t(process.argv)}else if(n==="mcp"){const{mcpCLI:t,ensureBundledMcpServers:a}=await import("../src/mcp-cli.mjs");a(),await t(process.argv)}process.exit(0)}function P(){let e=i.dirname(u(import.meta.resolve("@earendil-works/pi-coding-agent")));for(;!r.existsSync(i.join(e,"package.json"));){const s=i.dirname(e);if(s===e)throw new Error("could not locate @earendil-works/pi-coding-agent package root");e=s}return e}const y=P(),g=i.join(y,"package.json"),p=JSON.parse(r.readFileSync(g,"utf8")),f=p.bin,O=typeof f=="string"?f:f.pi,T=i.join(y,O),h="Naraya";function x(){try{if(p.piConfig?.name===h)return;p.piConfig={...p.piConfig??{},name:h},r.writeFileSync(g,JSON.stringify(p,null,2))}catch{}}x();const m=i.dirname(i.dirname(u(import.meta.url))),S=i.join(m,"assets");if(r.existsSync(S))N(S,o);else{const{readPack:e}=await import("../src/assets-pack.mjs");R(e(i.join(m,"dist","assets.pack.gz")),o)}function I(){const e=i.join(o,"settings.json");let s={};try{s=JSON.parse(r.readFileSync(e,"utf8"))}catch{}let t=!1;s.quietStartup===void 0&&(s.quietStartup=!0,t=!0),s.narayaThemeApplied||(s.theme="naraya",s.narayaThemeApplied=!0,t=!0),s.retry===void 0&&(s.retry={enabled:!0,maxRetries:10,baseDelayMs:1e3},t=!0),t&&(r.mkdirSync(o,{recursive:!0}),r.writeFileSync(e,JSON.stringify(s,null,2)))}I();const b=i.join(o,"SYSTEM.md"),F=i.join(o,"APPEND-SYSTEM.md");function _(e){let s=!1;try{s=!!JSON.parse(r.readFileSync(i.join(o,"models.json"),"utf8")).providers?.naraya}catch{}if(!s)return[];const t=[];return e.includes("--provider")||t.push("--provider","naraya"),e.includes("--models")||t.push("--models","naraya/*"),t}const l=process.argv.slice(2),B=["--version","-v","--help","-h","--list-models","--models"],d=l.some(e=>B.includes(e));if(!d)if(process.stdout.isTTY){const{splash:e}=await import("../src/splash.mjs"),s=await e();if(!s.ok)if(s.reason==="login"||s.reason==="expired"){const{resolveBase:t}=await import("../src/config.mjs"),{login:a}=await import("../src/login.mjs");try{await a(o,t(process.argv))}catch(c){console.error(`Login gagal: ${c.message}`),process.exit(1)}}else process.exit(1)}else r.existsSync(i.join(o,"models.json"))||(console.error("Run `naraya login` first."),process.exit(1));if(!d)try{const e=i.join(o,"models.json"),t=JSON.parse(r.readFileSync(e,"utf8")).providers?.naraya;if(t?.apiKey&&t?.baseUrl){const a=await fetch(`${t.baseUrl}/models`,{headers:{authorization:`Bearer ${t.apiKey}`},signal:AbortSignal.timeout(3e3)});if(a.ok){const c=(await a.json()).data;if(Array.isArray(c)&&c.length>0){const{writeModelsConfig:j}=await import("../src/config.mjs");j(o,t.apiKey,c,t.baseUrl)}}}}catch{}if(!d)try{const{ensureBundledMcpServers:e}=await import("../src/mcp-cli.mjs");e()}catch{}if(!d&&process.stdout.isTTY)try{const{printBanner:e}=await import("../src/banner.mjs");await e()}catch{}const w=i.join(o,"skills"),D=l.includes("--no-skills")?[]:r.existsSync(w)?["--no-skills","--skill",w]:["--no-skills"],C=k(i.join(i.dirname(u(import.meta.url)),"undici-timeout.mjs")).href,E=l.includes("--system-prompt")?[]:["--system-prompt",b],Y=["--import",C,T,...E,"--append-system-prompt",F,...D,..._(l),...l],L=A(process.execPath,Y,{stdio:"inherit",env:{...process.env,PI_CODING_AGENT_DIR:o,NARAYA_CODING_AGENT_DIR:o,PI_OFFLINE:"1",NARAYA_PKG_ROOT:m,NARAYA_VERSION:(()=>{try{return JSON.parse(r.readFileSync(i.join(m,"package.json"),"utf8")).version??""}catch{return""}})()}});L.on("exit",async e=>{if(process.stdout.isTTY&&!d)try{const{printBye:s}=await import("../src/goodbye.mjs");s()}catch{}process.exit(e??0)});
@@ -0,0 +1 @@
1
+ import{setGlobalDispatcher as e,Agent as t}from"undici";e(new t({headersTimeout:0,bodyTimeout:0,keepAliveTimeout:6e4,keepAliveMaxTimeout:6e5,connect:{timeout:3e4}}));
Binary file
@@ -0,0 +1,32 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ export function loadMcpConfig(projectDir, globalDir) {
4
+ const globalPath = path.join(globalDir, "mcp.json");
5
+ const projectPath = path.join(projectDir, ".mcp.json");
6
+ let globalConfig = { mcpServers: {} };
7
+ let projectConfig = { mcpServers: {} };
8
+ try {
9
+ if (fs.existsSync(globalPath)) {
10
+ globalConfig = JSON.parse(fs.readFileSync(globalPath, "utf8"));
11
+ }
12
+ }
13
+ catch {
14
+ // Ignore malformed global config
15
+ }
16
+ try {
17
+ if (fs.existsSync(projectPath)) {
18
+ projectConfig = JSON.parse(fs.readFileSync(projectPath, "utf8"));
19
+ }
20
+ }
21
+ catch {
22
+ // Ignore malformed project config
23
+ }
24
+ // Merge: project wins on conflict
25
+ const merged = {
26
+ mcpServers: {
27
+ ...globalConfig.mcpServers,
28
+ ...projectConfig.mcpServers
29
+ }
30
+ };
31
+ return merged;
32
+ }
@@ -0,0 +1,90 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { createTransport } from "./transport.js";
3
+ export class McpManager {
4
+ config;
5
+ servers = new Map();
6
+ projectDir;
7
+ constructor(config, projectDir) {
8
+ this.config = config;
9
+ this.projectDir = projectDir;
10
+ }
11
+ async start() {
12
+ const serverEntries = Object.entries(this.config.mcpServers ?? {});
13
+ for (const [name, serverConfig] of serverEntries) {
14
+ await this.startServer(name, serverConfig);
15
+ }
16
+ }
17
+ async startServer(name, config) {
18
+ try {
19
+ const transport = await createTransport(config, this.projectDir);
20
+ const client = new Client({ name: "naraya-cli", version: "0.2.4" }, { capabilities: {} });
21
+ const connection = {
22
+ client,
23
+ transport,
24
+ config,
25
+ tools: [],
26
+ status: "connecting"
27
+ };
28
+ this.servers.set(name, connection);
29
+ await client.connect(transport);
30
+ connection.status = "connected";
31
+ // Discover tools
32
+ const toolsResult = await client.listTools();
33
+ connection.tools = toolsResult.tools ?? [];
34
+ }
35
+ catch (err) {
36
+ const connection = this.servers.get(name);
37
+ if (connection) {
38
+ connection.status = "failed";
39
+ connection.error = err.message;
40
+ }
41
+ console.error(`[MCP] Failed to start server '${name}': ${err.message}`);
42
+ }
43
+ }
44
+ async stop() {
45
+ for (const [name, conn] of this.servers.entries()) {
46
+ try {
47
+ if (conn.status === "connected") {
48
+ await conn.client.close();
49
+ }
50
+ conn.status = "disconnected";
51
+ }
52
+ catch (err) {
53
+ console.error(`[MCP] Error stopping server '${name}': ${err.message}`);
54
+ }
55
+ }
56
+ this.servers.clear();
57
+ }
58
+ getServerStatus() {
59
+ const result = {};
60
+ for (const [name, conn] of this.servers.entries()) {
61
+ result[name] = {
62
+ status: conn.status,
63
+ toolCount: conn.tools.length,
64
+ error: conn.error
65
+ };
66
+ }
67
+ return result;
68
+ }
69
+ async callTool(serverName, toolName, params) {
70
+ const conn = this.servers.get(serverName);
71
+ if (!conn) {
72
+ throw new Error(`MCP server '${serverName}' not found`);
73
+ }
74
+ if (conn.status !== "connected") {
75
+ throw new Error(`MCP server '${serverName}' is not connected (status: ${conn.status})`);
76
+ }
77
+ return await conn.client.callTool({ name: toolName, arguments: params });
78
+ }
79
+ getTools() {
80
+ const result = [];
81
+ for (const [serverName, conn] of this.servers.entries()) {
82
+ if (conn.status === "connected") {
83
+ for (const tool of conn.tools) {
84
+ result.push({ serverName, tool });
85
+ }
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ }
@@ -0,0 +1,31 @@
1
+ export function mapMcpToolToPi(serverName, mcpTool) {
2
+ const toolName = `mcp__${serverName}__${mcpTool.name}`;
3
+ const label = `${serverName}: ${mcpTool.name}`;
4
+ const description = mcpTool.description ?? `MCP tool: ${mcpTool.name}`;
5
+ return {
6
+ name: toolName,
7
+ label,
8
+ description,
9
+ parameters: mcpTool.inputSchema ?? { type: "object", properties: {} }
10
+ };
11
+ }
12
+ export function mapMcpResultToPi(result) {
13
+ const content = result.content ?? [];
14
+ return {
15
+ content: content.map((c) => {
16
+ if (c.type === "text")
17
+ return { type: "text", text: c.text };
18
+ if (c.type === "image")
19
+ return { type: "image", data: c.data, mimeType: c.mimeType };
20
+ return { type: "text", text: JSON.stringify(c) };
21
+ }),
22
+ details: {}
23
+ };
24
+ }
25
+ export function mapMcpErrorToPi(err) {
26
+ return {
27
+ content: [{ type: "text", text: `MCP error: ${err.message}` }],
28
+ isError: true,
29
+ details: {}
30
+ };
31
+ }