rynster 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/LICENSE +89 -0
- package/README.md +355 -2
- package/SECURITY.md +75 -0
- package/agent.config.example.json +35 -0
- package/dist/index.js +57 -0
- package/package.json +60 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Rynster release notes
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-10-18
|
|
4
|
+
|
|
5
|
+
Rynster connects MCP clients to local coding projects through stdio or an
|
|
6
|
+
authenticated HTTP endpoint. This first npm release uses the `rynster` package
|
|
7
|
+
name and executable.
|
|
8
|
+
|
|
9
|
+
- Register local projects and expose their file, search, Git, and workflow tools.
|
|
10
|
+
- Approve edits and configured development commands from the local terminal.
|
|
11
|
+
- Protect credentials and project boundaries; optionally work in Git worktrees.
|
|
12
|
+
- Connect remote clients through a user-managed HTTPS tunnel with project-scoped
|
|
13
|
+
OAuth grants, automatic refresh, and local grant listing/revocation commands.
|
|
14
|
+
- Diagnose HTTP OAuth discovery with `rynster doctor`.
|
|
15
|
+
- Display the optional Ko-fi link with `rynster support`.
|
|
16
|
+
- Protect Windows credentials with native ACLs, reject linked credential stores and alternate data streams, and support installed npm commands without a batch launcher.
|
|
17
|
+
- Apply Windows ACLs without loading PowerShell modules; retain strict file identities by requiring runtimes with the upstream libuv fix.
|
|
18
|
+
- Validate source line endings and exercise the release workflow on both Linux and Windows CI runners.
|
|
19
|
+
|
|
20
|
+
Supported runtimes are Node.js 22.17+ within 22.x and Node.js 24.2+ within 24.x. Linux and Windows
|
|
21
|
+
have separate verification records; macOS has not been verified. Configured project
|
|
22
|
+
scripts execute with the host user's permissions and are not OS-sandboxed. See
|
|
23
|
+
the packaged `SECURITY.md` for the trust boundary and remaining race limitations.
|
|
24
|
+
|
|
25
|
+
This release starts fresh with `RYNSTER_*` settings and `~/.rynster/oauth.json`.
|
|
26
|
+
Earlier development product settings and credential stores are not imported.
|
|
27
|
+
Claude/ChatGPT onboarding and reads were exercised with earlier development builds
|
|
28
|
+
under the old package name; those results do not verify live onboarding with this
|
|
29
|
+
renamed tarball. Additional ChatGPT token-renewal and other lifecycle checks are
|
|
30
|
+
deferred.
|
|
31
|
+
|
|
32
|
+
The CLI is proprietary and free to use for personal and internal business
|
|
33
|
+
purposes, including commercial work and client projects. Redistribution, resale,
|
|
34
|
+
modification, and offering the CLI itself as a service require separate written
|
|
35
|
+
permission. See the packaged LICENSE for complete terms. Third-party components
|
|
36
|
+
retain their own licenses. Rynster Connect remains proprietary under separate
|
|
37
|
+
terms and is not included in this release. The source repository remains private.
|
|
38
|
+
Copyright holder: Baskar Ellangudi. Report security vulnerabilities privately to
|
|
39
|
+
<baskar12ai@gmail.com>; see SECURITY.md for reporting instructions.
|
|
40
|
+
|
|
41
|
+
Support development: <https://ko-fi.com/bas12>.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Rynster CLI Proprietary License
|
|
2
|
+
Version 1.0
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Baskar Ellangudi. All rights reserved except as
|
|
5
|
+
expressly granted below.
|
|
6
|
+
|
|
7
|
+
1. Scope
|
|
8
|
+
|
|
9
|
+
This license applies to the Rynster command-line application distributed as
|
|
10
|
+
the official "rynster" npm package and to Rynster-owned code included in that
|
|
11
|
+
application (the "Software"). The copyright holders are the "Licensor".
|
|
12
|
+
The Software is proprietary software, provided free of charge for the uses
|
|
13
|
+
permitted below. It is not offered under an open-source license.
|
|
14
|
+
|
|
15
|
+
2. Permitted use
|
|
16
|
+
|
|
17
|
+
Subject to these terms, the Licensor grants you a non-exclusive,
|
|
18
|
+
non-transferable, royalty-free license to download, install, and run the
|
|
19
|
+
Software for your own personal use and for your organization's internal
|
|
20
|
+
business purposes, including commercial work and work on client projects.
|
|
21
|
+
You may make copies needed for those installations, internal deployment,
|
|
22
|
+
ordinary package caching, and backups, retaining this license and notices.
|
|
23
|
+
You may connect your own authorized MCP clients to your running installation
|
|
24
|
+
and use the documented configuration and integration interfaces.
|
|
25
|
+
|
|
26
|
+
No payment, subscription, or donation is required for these permitted uses
|
|
27
|
+
of this version. Your projects, files, and work produced using the Software
|
|
28
|
+
remain yours or their respective owners'; this license makes no ownership
|
|
29
|
+
claim over them and does not restrict their use or distribution.
|
|
30
|
+
|
|
31
|
+
3. Restrictions
|
|
32
|
+
|
|
33
|
+
Except with the Licensor's separate written permission, you may not:
|
|
34
|
+
|
|
35
|
+
- Redistribute, publish, sell, sublicense, rent, or transfer the Software or
|
|
36
|
+
copies of it to third parties, including as part of another product.
|
|
37
|
+
- Modify, translate, or create derivative versions of the Software.
|
|
38
|
+
- Offer the Software itself to third parties as a hosted or managed service.
|
|
39
|
+
- Remove or alter copyright, license, or other proprietary notices, or
|
|
40
|
+
represent the Software as your own product.
|
|
41
|
+
- Reverse engineer, decompile, or disassemble the Software, except to the
|
|
42
|
+
extent applicable law permits these activities despite this restriction.
|
|
43
|
+
|
|
44
|
+
These restrictions do not limit configuring the Software or using it to
|
|
45
|
+
create, edit, run, or distribute your own projects and client work as allowed
|
|
46
|
+
in section 2. They do not restrict the npm registry's hosting, copying, or
|
|
47
|
+
distribution of the official package as authorized by the Licensor under
|
|
48
|
+
the registry's applicable terms. Rights that applicable law does not permit
|
|
49
|
+
this license to restrict are unaffected.
|
|
50
|
+
|
|
51
|
+
4. Third-party components
|
|
52
|
+
|
|
53
|
+
Third-party components retain their own licenses and notices. This license
|
|
54
|
+
does not replace or restrict rights granted under those licenses. Applicable
|
|
55
|
+
notices accompany the relevant components or their installed packages.
|
|
56
|
+
|
|
57
|
+
5. Rynster Connect and other services
|
|
58
|
+
|
|
59
|
+
This license does not grant a license to Rynster Connect, its implementation,
|
|
60
|
+
or any other separately offered software or hosted service. Rynster Connect
|
|
61
|
+
remains proprietary and requires separate applicable terms for access and
|
|
62
|
+
use. No Connect access, subscription, hosting, or pricing commitment is
|
|
63
|
+
included with the free CLI license. Access to a private source repository,
|
|
64
|
+
if provided separately, does not itself grant redistribution or other rights.
|
|
65
|
+
|
|
66
|
+
6. Ownership and termination
|
|
67
|
+
|
|
68
|
+
The Licensor retains ownership of the Software and all rights not expressly
|
|
69
|
+
granted here. No trademark rights are granted beyond truthful identification
|
|
70
|
+
of the Software. This license terminates if you violate its terms; upon
|
|
71
|
+
termination, you must stop using the Software and delete your copies, except
|
|
72
|
+
copies you are legally required to retain. A separate agreement is required
|
|
73
|
+
for any additional rights. These terms apply to this version; future versions
|
|
74
|
+
or services may be offered under different terms.
|
|
75
|
+
|
|
76
|
+
7. Disclaimer and limitation of liability
|
|
77
|
+
|
|
78
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE SOFTWARE IS PROVIDED
|
|
79
|
+
"AS IS" AND "AS AVAILABLE", WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
80
|
+
IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
81
|
+
TITLE, AND NON-INFRINGEMENT. THE LICENSOR DOES NOT PROMISE SUPPORT, UPDATES,
|
|
82
|
+
AVAILABILITY, OR THAT THE SOFTWARE WILL BE SECURE OR ERROR-FREE.
|
|
83
|
+
|
|
84
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSOR SHALL NOT BE
|
|
85
|
+
LIABLE FOR ANY DAMAGES OR OTHER LIABILITY ARISING FROM THE SOFTWARE OR ITS
|
|
86
|
+
USE, INCLUDING LOSS OF DATA, PROFITS, OR BUSINESS, WHETHER IN CONTRACT,
|
|
87
|
+
TORT, OR OTHERWISE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
88
|
+
NOTHING IN THIS LICENSE EXCLUDES LIABILITY OR RIGHTS THAT APPLICABLE LAW
|
|
89
|
+
DOES NOT ALLOW TO BE EXCLUDED.
|
package/README.md
CHANGED
|
@@ -1,3 +1,356 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Rynster (`rynster`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A provider-neutral local coding agent with MCP (Model Context Protocol) as its external interface.
|
|
4
|
+
|
|
5
|
+
Rynster connects MCP clients to registered local projects through configured permissions, file guards, and local approvals:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Claude / Grok / Future AI
|
|
9
|
+
│
|
|
10
|
+
│ MCP (stdio or HTTPS + Bearer Auth)
|
|
11
|
+
▼
|
|
12
|
+
┌──────────────────────────────┐
|
|
13
|
+
│ Rynster Server │
|
|
14
|
+
│ │
|
|
15
|
+
│ MCP Router │
|
|
16
|
+
│ Session Manager │
|
|
17
|
+
│ Permission Engine │
|
|
18
|
+
│ Path & Secret Guards │
|
|
19
|
+
│ Tool Executor │
|
|
20
|
+
│ Git Worktree Manager │
|
|
21
|
+
│ Audit Logger │
|
|
22
|
+
└──────────────┬───────────────┘
|
|
23
|
+
│
|
|
24
|
+
▼
|
|
25
|
+
┌──────────────────────────────┐
|
|
26
|
+
│ Isolated Worktree / Project │
|
|
27
|
+
│ │
|
|
28
|
+
│ src/ │
|
|
29
|
+
│ package.json │
|
|
30
|
+
│ ... │
|
|
31
|
+
└──────────────────────────────┘
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Key Features
|
|
37
|
+
|
|
38
|
+
1. **Project File Boundaries (`PathGuard`)**:
|
|
39
|
+
- Resolves canonical file paths and handles symbolic links.
|
|
40
|
+
- Blocks path traversal attacks (`../`, `../../`, `/etc/passwd`, symlinks escaping project root).
|
|
41
|
+
- Projects are registered by ID; paths are checked against their registered root.
|
|
42
|
+
|
|
43
|
+
2. **Protected Secrets (`SecretGuard`)**:
|
|
44
|
+
- Automatically blocks read/write/search access to `.env*`, `*.pem`, `*.key`, `*.keystore`, `*.jks`, `credentials*`, `service-account*.json`, `id_rsa*`, etc.
|
|
45
|
+
- Blocks Rynster configuration, active OAuth/audit storage, `.aws`, `.ssh`, `.codex`, `.agents`, `.rynster`, alongside `.git`, `node_modules`, `.agent`, `.gradle`, and `Pods`. Custom `protectedFiles` paths/globs also apply to project tools.
|
|
46
|
+
|
|
47
|
+
3. **Optimistic Locking (`SHA-256 Revision Tracking`)**:
|
|
48
|
+
- Every file read returns a SHA-256 revision hash.
|
|
49
|
+
- `expectedRevision` is checked before proposing an edit. The captured contents, file identity, and directory identities are checked again after approval. Changed files abort with `FILE_CHANGED`; unsafe paths are rejected.
|
|
50
|
+
|
|
51
|
+
4. **Safe Editing & Approval Workflow (`project_patch`)**:
|
|
52
|
+
- Generates unified diffs and addition/deletion statistics (`+X / -Y`).
|
|
53
|
+
- Terminal approval prompt (`[y] approve`, `[n] reject`, `[d] full diff`, `[a] allow writes this session`).
|
|
54
|
+
- Operations requiring interactive approval fail closed in non-interactive environments.
|
|
55
|
+
|
|
56
|
+
5. **Git Worktree Isolation (`WorktreeManager`)**:
|
|
57
|
+
- Sessions can spin up dedicated git worktrees (`.agent/worktrees/<session-id>`) on isolated branches (`agent/<session-id>`).
|
|
58
|
+
- A requested worktree must be created successfully; failure stops the session. Workflow handles, defaults, and session write permissions are scoped to the authenticated caller and physical project root.
|
|
59
|
+
|
|
60
|
+
6. **Restricted Command Runner**:
|
|
61
|
+
- Predefined project command categories only (`typecheck`, `lint`, `test`, `format`, `build`), with local approval by default.
|
|
62
|
+
- Tools accept predefined command categories rather than arbitrary command strings.
|
|
63
|
+
- Commands use an executable and argument array with `shell: false`. Simple legacy strings are accepted; shell operators/substitutions require migration to an explicitly configured local script.
|
|
64
|
+
- A small inherited environment excludes credentials and runtime injection variables.
|
|
65
|
+
- Non-interactive settings (`CI=true`, `TERM=dumb`), command-specific timeouts, and output capping (100 KB per stream). Cancellation and limits terminate the process group on Linux. Automatic typechecking uses installed TypeScript; Rynster never downloads it through `npx`.
|
|
66
|
+
- Configured scripts execute as the host user; path guards and environment filtering do not provide OS-level process isolation.
|
|
67
|
+
|
|
68
|
+
7. **Modern MCP Transports**:
|
|
69
|
+
- **Local stdio** and **HTTP at `/mcp`**, using SDK v2 and protocol `2026-07-28`.
|
|
70
|
+
- Legacy initialization, `/sse`, `/messages`, and root MCP aliases are retired.
|
|
71
|
+
- Application editing session IDs are workflow handles bound to the owner token, OAuth grant, or local stdio caller. Token renewal retains grant ownership; another grant cannot list or use its sessions.
|
|
72
|
+
|
|
73
|
+
8. **Audit Logging**:
|
|
74
|
+
- Records operation metadata and patch statistics into private audit storage. Queries, command arguments, source contents, bearer tokens, and URL queries are excluded from routine logs.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Security defaults
|
|
79
|
+
|
|
80
|
+
- **HTTP MCP always requires authentication.** The `--no-auth` and `--auto-approve` CLI flags have been removed. HTTP tool requests must carry a valid Bearer token or authenticated OAuth access token.
|
|
81
|
+
- Old configuration files with `server.noAuth: true` or `server.autoApprove: true` will fail HTTP startup with a security error. Remove those settings (including stale values set to `true`). Stdio mode also refuses legacy `server.autoApprove: true`.
|
|
82
|
+
- An action governed by the `ask` policy requires approval at the local terminal. In a headless process without interactive approval, such actions are denied; an explicit `allow` policy may still permit its corresponding action.
|
|
83
|
+
- Loopback HTTP (`127.0.0.1`) can still be published through a tunnel or reverse proxy. Do not interpret loopback binding alone as Internet isolation or readiness for production.
|
|
84
|
+
- Local security acceptance is covered by automated hostile-client and filesystem fixtures on Linux and Windows, with tested runtimes recorded in the compatibility report. Real Claude/ChatGPT token renewal, issuer-change, and revocation acceptance remain tracked separately. See [Local security policy](SECURITY.md).
|
|
85
|
+
|
|
86
|
+
## Quickstart
|
|
87
|
+
|
|
88
|
+
### 1. Install the Local package
|
|
89
|
+
|
|
90
|
+
Install published releases from the npm registry:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install --global rynster
|
|
94
|
+
rynster --version
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Alternatively, install a release tarball:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install -g /path/to/rynster-0.1.0.tgz
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The npm package and executable are both named `rynster`. Current release status
|
|
104
|
+
and changes are recorded in [the release notes](CHANGELOG.md). Report security
|
|
105
|
+
vulnerabilities privately to <baskar12ai@gmail.com>; see the
|
|
106
|
+
[Local security policy](SECURITY.md) for reporting instructions.
|
|
107
|
+
|
|
108
|
+
Project configuration is stored in `agent.config.json` in your working directory.
|
|
109
|
+
Owner-token fallback and audit files use `.agent/`. OAuth grants use
|
|
110
|
+
`~/.rynster/oauth.json` by default. Use `RYNSTER_PUBLIC_URL`,
|
|
111
|
+
`RYNSTER_OAUTH_STORAGE_PATH`, and `RYNSTER_TOKEN` for environment configuration.
|
|
112
|
+
This development release starts fresh without importing earlier installations.
|
|
113
|
+
|
|
114
|
+
### Installation troubleshooting
|
|
115
|
+
|
|
116
|
+
Rynster's `@vscode/ripgrep` dependency supplies native search through a
|
|
117
|
+
platform-specific optional npm package such as `@vscode/ripgrep-linux-x64`.
|
|
118
|
+
The current dependency ships the binary inside that package; it does not download
|
|
119
|
+
it from GitHub in a postinstall script.
|
|
120
|
+
|
|
121
|
+
If installation or startup reports a missing ripgrep platform package, check that
|
|
122
|
+
your npm registry or corporate mirror provides the matching `@vscode/ripgrep-*`
|
|
123
|
+
package and that your proxy and certificate settings allow registry access.
|
|
124
|
+
Install on the target operating system and architecture with optional dependencies
|
|
125
|
+
enabled:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm install --global rynster --include=optional
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The same `--include=optional` flag works when installing a release tarball. Avoid
|
|
132
|
+
copying an existing `node_modules` directory between different platforms.
|
|
133
|
+
|
|
134
|
+
### Runtime requirements
|
|
135
|
+
|
|
136
|
+
Requires Node.js 22.17+ within the 22.x release line, or Node.js 24.2+ within 24.x, and npm 11+.
|
|
137
|
+
|
|
138
|
+
Windows private credential storage requires the built-in Windows PowerShell and
|
|
139
|
+
`whoami`/`icacls` security tools. Credential files use Windows ACLs; Linux retains
|
|
140
|
+
POSIX private-file permissions. See the security policy for platform limits.
|
|
141
|
+
|
|
142
|
+
### 2. Register a Project
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
rynster register my-app /path/to/my-app
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
List registered projects:
|
|
149
|
+
```bash
|
|
150
|
+
rynster projects
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 3. Start Rynster
|
|
154
|
+
|
|
155
|
+
#### Remote MCP Mode (modern HTTP):
|
|
156
|
+
```bash
|
|
157
|
+
rynster start my-app --port 3333
|
|
158
|
+
```
|
|
159
|
+
Output:
|
|
160
|
+
```
|
|
161
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
162
|
+
│ RYNSTER │
|
|
163
|
+
└──────────────────────────────────────────────────────────────┘
|
|
164
|
+
Project: my-app (react, branch: main)
|
|
165
|
+
Project Root: /path/to/my-app
|
|
166
|
+
MCP Endpoint: http://127.0.0.1:3333/mcp
|
|
167
|
+
Health Check: http://127.0.0.1:3333/health
|
|
168
|
+
Owner Token: Stored privately. Run rynster token to display it explicitly.
|
|
169
|
+
|
|
170
|
+
Permissions:
|
|
171
|
+
Read: ALLOW
|
|
172
|
+
Search: ALLOW
|
|
173
|
+
Write/Create: ASK (approval required)
|
|
174
|
+
Delete: DENY
|
|
175
|
+
Tests/Lint: ASK
|
|
176
|
+
Git Commit: ASK
|
|
177
|
+
Git Push: DENY
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Stdio Mode (for a client supporting MCP 2026-07-28):
|
|
181
|
+
```bash
|
|
182
|
+
rynster start my-app --stdio
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Connecting AI Clients
|
|
188
|
+
|
|
189
|
+
### Connecting a modern remote MCP client
|
|
190
|
+
|
|
191
|
+
Use a client supporting protocol `2026-07-28`. Rynster implements canonical
|
|
192
|
+
discovery, CIMD/DCR, browser-bound consent, project grants, and hardened token
|
|
193
|
+
persistence. Automated Linux fixtures exercise those flows and an isolated
|
|
194
|
+
installed-package check exercises the Rynster executable. macOS and Windows are
|
|
195
|
+
unverified.
|
|
196
|
+
|
|
197
|
+
Earlier live Claude/ChatGPT onboarding, tool listing, and fixture reads were
|
|
198
|
+
recorded using development builds under the old package name. Those results do
|
|
199
|
+
not verify live onboarding with the renamed Rynster tarball. Additional ChatGPT
|
|
200
|
+
token-renewal and other lifecycle checks remain deferred. Historical details are
|
|
201
|
+
in the Local OAuth runbook in the source repository.
|
|
202
|
+
|
|
203
|
+
1. Expose the local port through your user-managed HTTPS tunnel.
|
|
204
|
+
2. Start Rynster with the tunnel origin:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
rynster start my-app --public-url https://<your-tunnel-hostname>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
3. Configure the client endpoint as `https://<your-tunnel-hostname>/mcp`, using an authentication method supported by that client. Rynster accepts its owner bearer token or an issued OAuth access token.
|
|
211
|
+
4. When the quick-tunnel hostname changes, update the public URL, restart Rynster, and recreate the client connection for the new issuer.
|
|
212
|
+
|
|
213
|
+
The public origin precedence is `--public-url` → `RYNSTER_PUBLIC_URL` → `server.publicUrl`. Startup prints the effective connector URL. Run `rynster doctor --url https://<your-tunnel-hostname>/mcp` for read-only discovery checks. OAuth consent grants only the selected project. Only the current version 2 OAuth storage format is accepted; earlier formats are not imported or migrated.
|
|
214
|
+
|
|
215
|
+
### Compatibility changes
|
|
216
|
+
|
|
217
|
+
The supported protocol is `2026-07-28` only. Modern clients discover the server and send protocol/client metadata on each request. Legacy `initialize` requests receive an unsupported-version error. `/sse` and `/messages` return 410; `POST /` returns 405 and points to `/mcp`. `GET /` remains JSON information even when an SSE Accept header is supplied. There is no legacy fallback.
|
|
218
|
+
|
|
219
|
+
### Stdio command configuration
|
|
220
|
+
|
|
221
|
+
For a client that supports the modern protocol and command-based MCP servers, use the following command configuration. This is not a claim of verified Claude Desktop compatibility:
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"mcpServers": {
|
|
225
|
+
"rynster": {
|
|
226
|
+
"command": "rynster",
|
|
227
|
+
"args": [
|
|
228
|
+
"start",
|
|
229
|
+
"my-app",
|
|
230
|
+
"--stdio"
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Local OAuth grant management
|
|
240
|
+
|
|
241
|
+
List the OAuth permissions granted to connected clients:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
rynster oauth grants
|
|
245
|
+
rynster oauth grants --json
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The listing includes complete grant IDs, client identities, projects, public
|
|
249
|
+
issuers, grant status, expiry times, and successful refresh counts. It excludes
|
|
250
|
+
access/refresh tokens, token hashes, client secrets, and project-root hashes. An
|
|
251
|
+
`active` status means the stored grant is unexpired and unrevoked; it does not
|
|
252
|
+
prove the client is currently connected. Listings read a snapshot without changing
|
|
253
|
+
the store or migrating legacy data. A missing store produces an empty listing.
|
|
254
|
+
|
|
255
|
+
To cancel one connection's grant, copy its complete ID from the listing:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
rynster oauth revoke FULL-GRANT-ID
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Revocation blocks that grant's access tokens and refresh tokens, including while
|
|
262
|
+
Rynster is running, and persists across restarts. Other grants are preserved. The
|
|
263
|
+
client needs fresh consent to access the project again. These commands manage
|
|
264
|
+
OAuth grants; the separate owner bearer credential is unaffected. They are local
|
|
265
|
+
administration commands and are not exposed as MCP tools.
|
|
266
|
+
|
|
267
|
+
Both commands use `~/.rynster/oauth.json` by default. If Rynster was started with a
|
|
268
|
+
`RYNSTER_OAUTH_STORAGE_PATH` override, use that same environment setting in the
|
|
269
|
+
administrative terminal, or select the file explicitly:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
rynster oauth grants --storage /path/to/test-oauth.json
|
|
273
|
+
rynster oauth revoke FULL-GRANT-ID --storage /path/to/test-oauth.json
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Selection priority is `--storage` → `RYNSTER_OAUTH_STORAGE_PATH` → `~/.rynster/oauth.json`.
|
|
277
|
+
`--config` selects project configuration for other commands; it does not select
|
|
278
|
+
OAuth storage. Revocation requires an exact ID, reports unknown IDs as errors,
|
|
279
|
+
and is safe to repeat for an already-revoked grant. Malformed, legacy, or aliased
|
|
280
|
+
stores are rejected; revocation also respects the server's storage writer lock.
|
|
281
|
+
|
|
282
|
+
For a live restart test, first confirm a fresh client read, revoke its grant in
|
|
283
|
+
another terminal, restart Rynster with the same public URL and OAuth store, and
|
|
284
|
+
confirm reads still fail without fresh consent. Then reconnect the client and
|
|
285
|
+
verify a fresh read succeeds.
|
|
286
|
+
|
|
287
|
+
## Available MCP Tools
|
|
288
|
+
|
|
289
|
+
| Tool | Description |
|
|
290
|
+
| :--- | :--- |
|
|
291
|
+
| `project_info` | Returns project framework, package manager, and active git branch |
|
|
292
|
+
| `project_tree` | Compact ASCII tree view of directory structure (ignores noise/secrets) |
|
|
293
|
+
| `project_read` | Line-numbered file reading with line ranges, metadata, and SHA-256 revision hash |
|
|
294
|
+
| `project_search` | Structured ripgrep search returning file, line number, and snippet matches |
|
|
295
|
+
| `git_status` | Returns git status (clean, modified, staged, untracked) |
|
|
296
|
+
| `git_diff` | Returns unified git diff |
|
|
297
|
+
| `session_start` | Starts a session (optionally in an isolated git worktree branch) |
|
|
298
|
+
| `session_list` | Lists active sessions and read/changed file counters |
|
|
299
|
+
| `session_stop` | Cleans up session with action: `keep`, `commit`, or `discard` |
|
|
300
|
+
| `project_patch` | Safely patches a file, verifying revision hash and prompting approval |
|
|
301
|
+
| `project_typecheck` | Executes pre-configured typecheck command |
|
|
302
|
+
| `project_lint` | Executes pre-configured lint command |
|
|
303
|
+
| `project_test` | Executes pre-configured test command |
|
|
304
|
+
| `project_format` | Executes pre-configured formatting command |
|
|
305
|
+
| `project_build` | Executes pre-configured build command |
|
|
306
|
+
| `project_list` | Lists all registered projects in the agent |
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## License and distribution
|
|
311
|
+
|
|
312
|
+
The Rynster CLI is proprietary and free to use for personal and internal business
|
|
313
|
+
purposes, including commercial work and client projects, under the [Rynster CLI
|
|
314
|
+
Proprietary License](LICENSE). No subscription or donation is required for those
|
|
315
|
+
uses. Your projects and work remain yours or their respective owners'.
|
|
316
|
+
|
|
317
|
+
Redistributing, reselling, modifying, or offering the CLI itself as a hosted or
|
|
318
|
+
managed service requires separate written permission. Third-party components
|
|
319
|
+
retain their own licenses. See LICENSE for the complete terms and exceptions.
|
|
320
|
+
|
|
321
|
+
The GitHub source repository remains private. The public npm package contains
|
|
322
|
+
the runnable JavaScript CLI, which recipients can inspect. Public availability
|
|
323
|
+
does not grant an open-source license. Rynster Connect remains proprietary and
|
|
324
|
+
requires separate terms; the CLI license does not include Connect access.
|
|
325
|
+
|
|
326
|
+
## Support Rynster
|
|
327
|
+
|
|
328
|
+
You can [support Rynster's development on Ko-fi](https://ko-fi.com/bas12).
|
|
329
|
+
Support is optional. To display the link in your terminal:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
rynster support
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The link also appears in `rynster --help` and the npm package's funding metadata.
|
|
336
|
+
|
|
337
|
+
## Local command configuration and migration
|
|
338
|
+
|
|
339
|
+
Use structured commands in `agent.config.json`:
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"commands": {
|
|
344
|
+
"test": { "file": "npm", "args": ["test"] },
|
|
345
|
+
"typecheck": { "file": "npm", "args": ["run", "typecheck"] }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
This `commands` object belongs inside a registered project. For several commands, put the sequence in a locally reviewed script and configure its executable and arguments. An existing simple value such as `"npm test"` still works; values with `&&`, pipes, redirection, or substitutions are rejected. Executable resolution, package manifests/locks, and command configuration are captured before approval and rechecked before execution. New configurations ask for approval for every command category. Existing explicit `allow` policies remain an owner decision to trust project code, including code changed by an assistant.
|
|
351
|
+
|
|
352
|
+
Project scripts run with the host user's filesystem and network access, including home-directory files. Neither `shell: false`, environment filtering, a project directory, nor a Git worktree provides an OS sandbox. Review project code before approving execution.
|
|
353
|
+
|
|
354
|
+
Configuration is saved atomically with mode `0600` and an exclusive writer lock; malformed files and concurrent changes fail visibly. Duplicate project roots, including symlink aliases, are rejected. Startup masks the owner token; use `rynster token --config /path/to/agent.config.json` only when you intend to display it. After upgrading to physical-root grant binding, reconnect existing OAuth clients; earlier path-only grants are rejected.
|
|
355
|
+
|
|
356
|
+
See [Local security policy](SECURITY.md) for vulnerability reporting, the security model, and verification limits. Development roadmaps and historical verification records are maintained in the private source repository.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Rynster Local security policy
|
|
2
|
+
|
|
3
|
+
Reviewed on 2026-10-18. See the source compatibility report for tested Linux/Windows runtimes and remaining platform limits. Local runs on the developer's machine; Connect hosting is not part of this release.
|
|
4
|
+
|
|
5
|
+
## Reporting a vulnerability
|
|
6
|
+
|
|
7
|
+
Email **Baskar Ellangudi** privately at **[baskar12ai@gmail.com](mailto:baskar12ai@gmail.com)**
|
|
8
|
+
to report a suspected security vulnerability. This is the public reporting contact
|
|
9
|
+
for Rynster; the source repository is private, so reports do not require GitHub
|
|
10
|
+
access. Use the subject `Rynster security report`.
|
|
11
|
+
|
|
12
|
+
Include the installed Rynster version, Node.js version, operating system and
|
|
13
|
+
architecture, affected command or MCP tool, steps to reproduce, expected and
|
|
14
|
+
observed behavior, and the potential impact. Prefer a minimal reproduction using
|
|
15
|
+
temporary files and dummy credentials. Redact logs and configuration before
|
|
16
|
+
sharing them: do not send access or refresh tokens, owner credentials, client
|
|
17
|
+
secrets, private keys, or private project files. Coordinate any sensitive follow-up
|
|
18
|
+
material with the maintainer by email.
|
|
19
|
+
|
|
20
|
+
## Authorization and editing sessions
|
|
21
|
+
|
|
22
|
+
HTTP MCP requires an owner bearer token or a valid OAuth grant. OAuth grants authorize one project and bind to its canonical root plus filesystem device/inode identity. Revocation, expiry, issuer changes, and physical root replacement deny further tool execution. Earlier path-only project grants require fresh consent after this upgrade.
|
|
23
|
+
|
|
24
|
+
Editing sessions, default sessions, and session-wide write permissions belong to an authenticated principal. The owner token, each OAuth grant, and the local stdio caller have separate ownership. Refreshing a token retains its grant's workflow ownership. Another grant cannot list, read through, patch through, execute commands through, or stop its sessions. Session IDs use random UUIDs and are never credentials.
|
|
25
|
+
|
|
26
|
+
Read/search policies also cover trees, metadata, and Git tools. Explicitly requested worktree isolation fails closed; creation failure never falls back to the original project. Worktree creation, commit, and discard respect local create/commit/delete policies. Cleanup failures remain visible for recovery. Duplicate canonical project roots, including symlink aliases, are refused.
|
|
27
|
+
|
|
28
|
+
## File and credential protection
|
|
29
|
+
|
|
30
|
+
Project reads, searches, trees, patches, and Git diffs protect credential patterns, Rynster configuration, active OAuth/audit storage, and sensitive directories such as `.agent`, `.rynster`, `.aws`, `.ssh`, `.codex`, and `.agents`. Local `protectedFiles` paths/globs extend these rules and carry into worktrees. Git diff filters changed paths before retrieving contents, uses literal pathspecs, validates base revisions, and disables external diff/textconv programs. Git operations disable hooks and fsmonitor where applicable.
|
|
31
|
+
|
|
32
|
+
Patches capture contents, file identity, and existing directory identities before approval. They recheck those values, authorization, and permissions before writing. Symlink patch paths and hard-linked targets are refused. Replacing a file or directory, editing contents, or creating a formerly absent target during approval invalidates the proposed edit. Atomic temporary-file replacement avoids exposing a partially written file; Linux writes pin the parent through a directory descriptor.
|
|
33
|
+
|
|
34
|
+
These checks mitigate approval-time and path-substitution races. They do not provide kernel-level compare-and-swap or protection against an unrestricted malicious local process changing the filesystem during the final synchronous commit. Local's threat model trusts the OS user and approved project code. macOS remains unverified.
|
|
35
|
+
|
|
36
|
+
Configuration uses atomic private replacement, an exclusive writer lock, and detection of concurrent edits. Malformed/unreadable configuration fails startup rather than silently applying defaults. OAuth persistence remains versioned, private, hashed, locked, and atomic. Startup does not print the owner token. `rynster token` explicitly displays it for local consent; treat that output as a credential.
|
|
37
|
+
|
|
38
|
+
On Windows, private credential files use protected ACLs granting access to the current user, SYSTEM, and local Administrators. Newly created application storage directories receive the same protection; existing project or parent directories are not recursively repermissioned. Built-in Windows PowerShell is required to apply and verify these ACLs. Failure to establish permissions denies storage access. Read-only grant listing does not change permissions. Explicit file/descriptor identity checks reject linked stores; Windows additionally rejects credential paths through junctions and project paths addressing alternate data streams. These checks retain the local-user race limitation above and do not claim the same kernel protections as Linux `O_NOFOLLOW` or directory-descriptor pinning.
|
|
39
|
+
|
|
40
|
+
Both systems flush private file data before same-directory replacement. Linux also flushes the OAuth directory after replacement. Windows omits that directory flush because Node cannot perform it here; identical power-loss durability across operating systems is not claimed. Storage errors remain failures and are never treated as successful writes. As on Linux, an error after replacement can occur after the new file has become visible.
|
|
41
|
+
|
|
42
|
+
## Approval and command execution
|
|
43
|
+
|
|
44
|
+
A local approval request has a unique operation ID/digest, project/root/revision context, authenticated owner, and five-minute expiry. Its decision is valid only for the captured operation. Changed revisions, paths, executable identity, package manifests/locks, or command configuration require a fresh request. Cancellation, expiry, and grant revocation deny dispatch. Terminal prompts are serialized and use stderr, so MCP stdio frames remain intact. Closed input and headless processes deny approval-required operations. No global automatic-approval flag is supported.
|
|
45
|
+
|
|
46
|
+
New configurations ask before every command category. The only categories exposed to clients are `typecheck`, `lint`, `test`, `format`, and `build`; remote clients cannot supply executable names or command arguments. Local configuration specifies `{ "file": "npm", "args": ["test"] }`. Simple legacy strings such as `npm test` still work, but shell operators and substitutions are refused. Put command sequences into a locally reviewed script instead.
|
|
47
|
+
|
|
48
|
+
Rynster launches resolved executables with `shell: false`, an environment allowlist, closed stdin, bounded output, and timeouts. It does not implicitly install TypeScript using `npx`. On Linux, cancellation, timeout, and output overflow terminate the process group, including ordinary descendants. Processes that deliberately escape their group are outside this trusted-code policy.
|
|
49
|
+
|
|
50
|
+
On Windows, npm commands run through Node and the installed npm CLI JavaScript entry point. Both executable and entry-point identity/content are checked across approval. Arbitrary batch launchers are refused; configure their real executable and script arguments explicitly. Quoted Windows executable paths are supported in legacy command strings; structured commands remain preferred. Windows process termination does not provide the Linux process-group descendant cleanup guarantee.
|
|
51
|
+
|
|
52
|
+
**Project scripts execute as the host user with host filesystem and network access.** npm itself can execute scripts through a shell. An executable/argument array, environment filtering, a working directory, and a Git worktree are not an OS sandbox. Existing explicit `allow` policies mean the owner trusts project code, including code changed by an assistant. Review that code before approving execution. No managed or hosted sandbox guarantee is claimed.
|
|
53
|
+
|
|
54
|
+
## HTTP and logging
|
|
55
|
+
|
|
56
|
+
Host validation restricts accepted hostnames. Origin checks require an exact public origin or the same local origin; null/untrusted origins are refused. CORS does not use a wildcard. Query credentials are never accepted. Routine request/audit logs omit URL queries, untrusted tool/session labels, search strings, command arguments, source contents, and tokens. Project scripts can still print secrets they choose to read; approving them grants their host capabilities.
|
|
57
|
+
|
|
58
|
+
## Acceptance and release limits
|
|
59
|
+
|
|
60
|
+
Local owners can inspect credential-free grant metadata with `rynster oauth grants`
|
|
61
|
+
and revoke an exact grant ID with `rynster oauth revoke <grant-id>`. The commands use
|
|
62
|
+
the same default OAuth store and `RYNSTER_OAUTH_STORAGE_PATH` override as the server,
|
|
63
|
+
with an explicit `--storage` option. Only Rynster settings and the current storage
|
|
64
|
+
format are supported. Earlier stores are not imported or migrated.
|
|
65
|
+
Listings do not write, migrate, or change file
|
|
66
|
+
permissions. Revocations use the existing writer lock and atomic private save,
|
|
67
|
+
reject unknown IDs and invalid/legacy storage, and remain effective after restart.
|
|
68
|
+
The commands are not exposed as MCP tools and do not revoke the separate owner
|
|
69
|
+
bearer credential. Client labels in terminal output are sanitized. CLI integration
|
|
70
|
+
tests confirm that a targeted revocation blocks access and refresh while retaining
|
|
71
|
+
another grant's access; live-client acceptance remains a separate check.
|
|
72
|
+
|
|
73
|
+
The automated suite covers HTTP/stdio authorization, same-project grant ownership, refresh ownership, physical root replacement, credential guards, stale/canceled/expired approvals, symlink replacement, approved edits, headless denial, command grammar/environment/output/process cleanup, and configuration races. Installed-tarball checks cover production-only dependency resolution, CLI configuration, native search assets, workflow handles, secret guards, headless command denial, protocol rejection, and shutdown.
|
|
74
|
+
|
|
75
|
+
Earlier development builds under the old package name had user-confirmed real Claude/ChatGPT onboarding, tool listing, and fixture reads. Claude automatic renewal and recovery through a fresh connector after a hostname change are recorded in the private Local OAuth runbook. The user also reported immediate access denial after revoking Claude's grant, a new authorization prompt after restart, and a successful read after fresh consent. On 2026-10-15 the user confirmed the manual helper rejected the same unexpired OAuth token at the new issuer after an HTTP 200 baseline at the old issuer. These historical results do not verify live onboarding with the renamed Rynster tarball. ChatGPT lifecycle checks remain unverified and deferred at the user's request. Passing automated fixtures does not supply live-client evidence. See the source compatibility report for dated Windows/Linux automated and installed-package results; macOS remains unverified. Connect's authenticated headless approval channel and any OS-isolated executor remain future work.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"server": {
|
|
3
|
+
"port": 3333,
|
|
4
|
+
"host": "127.0.0.1"
|
|
5
|
+
},
|
|
6
|
+
"defaultProject": "my-project",
|
|
7
|
+
"projects": {
|
|
8
|
+
"my-project": {
|
|
9
|
+
"root": "./",
|
|
10
|
+
"description": "My local project",
|
|
11
|
+
"commands": {
|
|
12
|
+
"typecheck": { "file": "npm", "args": ["run", "typecheck"] },
|
|
13
|
+
"test": { "file": "npm", "args": ["test"] }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"audit": {
|
|
18
|
+
"enabled": true,
|
|
19
|
+
"logFile": ".agent/audit.log"
|
|
20
|
+
},
|
|
21
|
+
"permissions": {
|
|
22
|
+
"project.read": "allow",
|
|
23
|
+
"project.search": "allow",
|
|
24
|
+
"project.write": "ask",
|
|
25
|
+
"project.create": "ask",
|
|
26
|
+
"project.delete": "deny",
|
|
27
|
+
"command.typecheck": "ask",
|
|
28
|
+
"command.lint": "ask",
|
|
29
|
+
"command.test": "ask",
|
|
30
|
+
"command.format": "ask",
|
|
31
|
+
"command.build": "ask",
|
|
32
|
+
"git.commit": "ask",
|
|
33
|
+
"git.push": "deny"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var zt=Object.defineProperty;var g=(a,e)=>zt(a,"name",{value:e,configurable:!0});var j=(a,e,t)=>()=>{if(t)throw t[0];try{return a&&(e=a(a=0)),e}catch(r){throw t=[r],r}};var Bt=(a,e)=>{for(var t in e)zt(a,t,{get:e[t],enumerable:!0})};function Gt(a,e){let t=/^"([^"]+)","(S-\d+(?:-\d+)+)"\s*$/.exec(a.trim());if(!t)throw new Error("Cannot determine the Windows credential owner");let[,r,o]=t,n=r.toLowerCase().startsWith(`${e.toLowerCase()}\\`),s=/^S-1-5-21-\d+-\d+-\d+-(500|501)$/.exec(o)?.[1];return{sid:o,localAlias:n&&s?s==="500"?"LA":"LG":void 0}}function Jt(a,e,t){let r=/^D:([A-Z]*)(.*)$/.exec(a);if(!r||!r[1].includes("P"))return!1;let o=new Map([["SY","S-1-5-18"],["BA","S-1-5-32-544"]]);t.localAlias&&o.set(t.localAlias,t.sid);let n=new Set([t.sid,"S-1-5-18","S-1-5-32-544"]),s=r[2].match(/\([^)]*\)/g)||[];if(s.length!==n.size||s.join("")!==r[2])return!1;for(let c of s){let[i,l,d,u,p,f]=c.slice(1,-1).split(";");if(i!=="A"||l!==(e?"OICI":"")||d!=="FA"||u!==""||p!==""||!f||!n.delete(o.get(f)||f))return!1}return n.size===0}var Kt=j(()=>{"use strict";g(Gt,"windowsAclOwner");g(Jt,"isPrivateWindowsDacl")});import C from"node:fs";import re from"node:path";import qt from"node:os";import{spawnSync as Zt}from"node:child_process";function Yt(a,e,t){let r=Zt(re.join(a,"System32",e),t,{encoding:"utf8",timeout:1e4,windowsHide:!0,env:{SystemRoot:a,WINDIR:a}});if(r.error||r.status!==0)throw new Error("Cannot inspect private Windows ACL");return r.stdout}function Vt(a,e,t){if(!ut){let n=Yt(t,"whoami.exe",["/user","/fo","csv","/nh"]);ut=Gt(n,qt.hostname())}let r=C.mkdtempSync(re.join(qt.tmpdir(),"rynster-acl-")),o=re.join(r,"permissions.txt");try{Yt(t,"icacls.exe",[re.resolve(a),"/save",o,"/q"]);let n=C.readFileSync(o).toString("utf16le").replace(/^\uFEFF/,"").trim().split(/\r?\n/);return n.length===2&&Jt(n[1],e,ut)}finally{try{C.unlinkSync(o)}catch{}C.rmdirSync(r)}}function ze(a){let e=C.lstatSync(a);if(!e.isFile()||e.isSymbolicLink()||e.nlink!==1)throw new Error("Private storage must be a regular file without links");return e}function Be(a,e){return a.dev===e.dev&&a.ino===e.ino}function Xt(a){let e=new Map;if(process.platform!=="win32")return e;let t=re.dirname(re.resolve(a));for(;;){let r=C.lstatSync(t);if(!r.isDirectory()||r.isSymbolicLink())throw new Error("Private storage directories must not contain links or junctions");e.set(t,r);let o=re.dirname(t);if(o===t)return e;t=o}}function gt(a){for(let[e,t]of a){let r=C.lstatSync(e);if(!r.isDirectory()||r.isSymbolicLink()||!Be(t,r))throw new Error("Private storage directory changed")}}function Z(a,e=!1){let t=Xt(a),r=e?C.lstatSync(a):ze(a);if(r.isSymbolicLink()||e&&!r.isDirectory())throw new Error("Private directory must not be a link");if(process.platform==="win32"){let n=process.env.SystemRoot||process.env.WINDIR;if(!n)throw new Error("Cannot locate Windows ACL tooling");if(!Vt(a,e,n)){let s=Zt(re.join(n,"System32/WindowsPowerShell/v1.0/powershell.exe"),["-NoLogo","-NoProfile","-NonInteractive","-Command",Wr],{windowsHide:!0,timeout:15e3,encoding:"utf8",env:{SystemRoot:n,WINDIR:n,TEMP:process.env.TEMP,TMP:process.env.TMP,RYNSTER_PRIVATE_PATH:re.resolve(a)}});if(s.error||s.status!==0)throw new Error("Cannot apply private Windows ACL; storage access denied",{cause:s.error||new Error(s.stderr.trim())});if(!Vt(a,e,n))throw new Error("Private Windows ACL verification failed; storage access denied")}}else C.chmodSync(a,e?448:384);let o=e?C.lstatSync(a):ze(a);if(gt(t),o.isSymbolicLink()||!Be(r,o))throw new Error("Private storage changed while setting permissions")}function le(a){let e=Xt(a),t=ze(a),r=C.openSync(a,C.constants.O_RDONLY|(C.constants.O_NOFOLLOW||0)|(C.constants.O_NONBLOCK||0));try{let o=C.fstatSync(r);if(gt(e),!o.isFile()||o.nlink!==1||!Be(t,o))throw new Error("Private storage changed before reading");let n=C.readFileSync(r,"utf8");if(gt(e),!Be(o,ze(a)))throw new Error("Private storage changed while reading");return n}catch(o){throw new Error("Private storage changed or became unreadable",{cause:o})}finally{C.closeSync(r)}}function Qt(a){C.mkdirSync(a,{recursive:!0,mode:448})!==void 0&&Z(a,!0);let e=C.lstatSync(a);if(!e.isDirectory()||e.isSymbolicLink())throw new Error("Private storage directory must not be a link")}function er(a){if(process.platform==="win32")return;let e=C.openSync(a,"r");try{C.fsyncSync(e)}finally{C.closeSync(e)}}var Wr,ut,ht=j(()=>{"use strict";Kt();Wr=`
|
|
3
|
+
$ErrorActionPreference = 'Stop'
|
|
4
|
+
$PSModuleAutoLoadingPreference = 'None'
|
|
5
|
+
$attributes = [IO.File]::GetAttributes($env:RYNSTER_PRIVATE_PATH)
|
|
6
|
+
if ($attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Private path is a link' }
|
|
7
|
+
$directory = ($attributes -band [IO.FileAttributes]::Directory) -ne 0
|
|
8
|
+
$user = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
9
|
+
$sids = @($user.Value, 'S-1-5-18', 'S-1-5-32-544')
|
|
10
|
+
if ($directory) {
|
|
11
|
+
$acl = [IO.Directory]::GetAccessControl($env:RYNSTER_PRIVATE_PATH, [Security.AccessControl.AccessControlSections]::Access)
|
|
12
|
+
$inherit = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
|
|
13
|
+
} else {
|
|
14
|
+
$acl = [IO.File]::GetAccessControl($env:RYNSTER_PRIVATE_PATH, [Security.AccessControl.AccessControlSections]::Access)
|
|
15
|
+
$inherit = [Security.AccessControl.InheritanceFlags]::None
|
|
16
|
+
}
|
|
17
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
18
|
+
foreach ($rule in $acl.GetAccessRules($true, $false, [Security.Principal.SecurityIdentifier])) {
|
|
19
|
+
$acl.RemoveAccessRuleSpecific($rule)
|
|
20
|
+
}
|
|
21
|
+
foreach ($sid in $sids) {
|
|
22
|
+
$identity = [Security.Principal.SecurityIdentifier]::new($sid)
|
|
23
|
+
$rule = [Security.AccessControl.FileSystemAccessRule]::new($identity, 'FullControl', $inherit, 'None', 'Allow')
|
|
24
|
+
$acl.AddAccessRule($rule)
|
|
25
|
+
}
|
|
26
|
+
if ($directory) { [IO.Directory]::SetAccessControl($env:RYNSTER_PRIVATE_PATH, $acl) }
|
|
27
|
+
else { [IO.File]::SetAccessControl($env:RYNSTER_PRIVATE_PATH, $acl) }
|
|
28
|
+
`;g(Yt,"nativeWindowsTool");g(Vt,"hasPrivateWindowsAcl");g(ze,"regularFile");g(Be,"sameFile");g(Xt,"privateParents");g(gt,"verifyParents");g(Z,"protectPrivatePath");g(le,"readPrivateFile");g(Qt,"createPrivateDirectory");g(er,"syncPrivateDirectory")});import{z as k}from"zod";var be,Ur,Ge,tr=j(()=>{"use strict";be=k.union([k.string().min(1),k.object({file:k.string().min(1),args:k.array(k.string()).optional()}).strict()]),Ur=k.object({root:k.string().min(1,"Project root path is required"),description:k.string().optional(),commands:k.object({typecheck:be.optional(),lint:be.optional(),test:be.optional(),format:be.optional(),build:be.optional()}).catchall(be).optional(),protectedFiles:k.array(k.string()).optional(),ignoredPaths:k.array(k.string()).optional()}),Ge=k.object({server:k.object({port:k.number().int().min(0).max(65535).default(3333),host:k.string().default("127.0.0.1"),authToken:k.string().optional(),publicUrl:k.string().optional(),autoApprove:k.boolean().optional(),noAuth:k.boolean().optional()}).default({port:3333,host:"127.0.0.1"}),defaultProject:k.string().optional(),projects:k.record(k.string(),Ur).default({}),audit:k.object({enabled:k.boolean().default(!0),logFile:k.string().default(".agent/audit.log")}).default({enabled:!0,logFile:".agent/audit.log"}),permissions:k.record(k.string(),k.enum(["allow","ask","deny"])).default({"project.read":"allow","project.search":"allow","project.write":"ask","project.create":"ask","project.delete":"deny","command.typecheck":"ask","command.lint":"ask","command.test":"ask","command.format":"ask","command.build":"ask","git.commit":"ask","git.push":"deny"})})});var rr=j(()=>{"use strict"});import{z as y}from"zod";var ft,or=j(()=>{"use strict";ft={project_info:{description:"Returns metadata about a registered project (framework, package manager, git branch)",inputSchema:y.object({project:y.string().optional().describe("Registered project ID (optional if default project set)")})},project_list:{description:"Lists all projects registered with this agent",inputSchema:y.object({})},project_tree:{description:"Returns a compact directory tree of the project or subpath, filtering out noise and secrets",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),path:y.string().optional().default(".").describe("Subdirectory path (relative)"),depth:y.number().optional().default(3).describe("Max depth of tree traversal")})},project_read:{description:"Safely reads a slice of a file with line numbers, metadata, and an optimistic revision hash",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),path:y.string().describe("Relative path to file inside project"),startLine:y.number().optional().describe("1-indexed starting line number"),endLine:y.number().optional().describe("1-indexed ending line number"),sessionId:y.string().optional().describe("Session ID to bind read tracking")})},project_search:{description:"Performs fast ripgrep search returning structured matches (path, line number, snippet)",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),query:y.string().describe("Search query string"),path:y.string().optional().default(".").describe("Subdirectory to restrict search"),maxResults:y.number().optional().default(30).describe("Maximum matches to return"),caseSensitive:y.boolean().optional().default(!1).describe("Case sensitive search"),sessionId:y.string().optional().describe("Session ID")})},git_status:{description:"Retrieves git status for the project or active session worktree",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),sessionId:y.string().optional().describe("Session ID (if using worktree)")})},git_diff:{description:"Returns git diff for working tree or specific file in project or session worktree",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),path:y.string().optional().describe("Specific file path to diff"),staged:y.boolean().optional().default(!1).describe("Diff staged changes"),base:y.string().optional().describe("Base commit/branch to diff against"),sessionId:y.string().optional().describe("Session ID")})},session_start:{description:"Starts a new agent session, optionally creating an isolated git worktree for safe editing",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),useWorktree:y.boolean().optional().default(!1).describe("Create isolated git worktree branch"),baseBranch:y.string().optional().describe("Base branch for worktree")})},session_list:{description:"Lists active sessions and their metadata",inputSchema:y.object({})},session_stop:{description:"Stops an agent session and cleans up or commits its worktree",inputSchema:y.object({sessionId:y.string().describe("Session ID to stop"),action:y.enum(["keep","commit","discard"]).default("keep").describe("Worktree action: 'commit' saves to branch, 'discard' deletes worktree & branch, 'keep' preserves"),commitMessage:y.string().optional().describe("Commit message if action is 'commit'")})},project_patch:{description:"Safely modifies a file using optimistic revision locking, generating diff and requesting approval",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),path:y.string().describe("Relative path to file"),patch:y.string().describe("Unified diff or <<<<<<< SEARCH ... ======= ... >>>>>>> REPLACE patch"),reason:y.string().optional().describe("Reason for this change"),expectedRevision:y.string().optional().describe("SHA-256 revision hash from project_read for optimistic locking"),sessionId:y.string().optional().describe("Session ID")})},project_typecheck:{description:"Runs the configured typecheck command (e.g. tsc)",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),sessionId:y.string().optional().describe("Session ID (runs inside worktree if active)")})},project_lint:{description:"Runs the configured lint command (e.g. eslint)",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),sessionId:y.string().optional().describe("Session ID (runs inside worktree if active)")})},project_test:{description:"Runs the configured test suite",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),sessionId:y.string().optional().describe("Session ID (runs inside worktree if active)")})},project_format:{description:"Runs the configured format command (e.g. prettier)",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),sessionId:y.string().optional().describe("Session ID (runs inside worktree if active)")})},project_build:{description:"Runs the configured build command",inputSchema:y.object({project:y.string().optional().describe("Registered project ID"),sessionId:y.string().optional().describe("Session ID (runs inside worktree if active)")})}}});var mt=j(()=>{"use strict";tr();rr();or()});import A from"node:fs";import je from"node:path";import nr from"node:crypto";var Hr,q,yt=j(()=>{"use strict";ht();mt();Hr="agent.config.json",q=class{static{g(this,"ConfigManager")}config;configPath;loadedContents;constructor(e){this.configPath=je.resolve(e||this.findConfigFile()),this.config=this.loadConfig()}findConfigFile(){let e=je.resolve(process.cwd(),Hr);return A.existsSync(e),e}loadConfig(){if(A.existsSync(this.configPath))try{let e=A.lstatSync(this.configPath);if(!e.isFile()||e.isSymbolicLink()||e.nlink!==1)throw new Error("Configuration must be a regular file without aliases");let t=le(this.configPath);this.loadedContents=t;let r=JSON.parse(t),o=Ge.parse(r);return Z(this.configPath),o}catch{throw new Error("Configuration is invalid or unreadable; repair it before starting Rynster")}return Ge.parse({})}saveConfig(){let e=je.dirname(this.configPath);A.existsSync(e)||A.mkdirSync(e,{recursive:!0});let t,r;try{if(t=A.openSync(`${this.configPath}.lock`,"wx",384),A.existsSync(this.configPath)&&A.lstatSync(this.configPath).isSymbolicLink())throw new Error("Configuration path changed");if((A.existsSync(this.configPath)?le(this.configPath):void 0)!==this.loadedContents)throw new Error("Configuration changed in another process; reload and retry");let n=JSON.stringify(Ge.parse(this.config),null,2);r=`${this.configPath}.${nr.randomUUID()}.tmp`;let s=A.openSync(r,"wx",384);try{Z(r),A.writeFileSync(s,n),A.fsyncSync(s)}finally{A.closeSync(s)}A.renameSync(r,this.configPath),r=void 0,this.loadedContents=n}finally{r&&A.unlinkSync(r),t!==void 0&&(A.closeSync(t),A.unlinkSync(`${this.configPath}.lock`))}}getConfigPath(){return this.configPath}getConfig(){return this.config}getOrCreateAuthToken(){if(process.env.RYNSTER_TOKEN)return process.env.RYNSTER_TOKEN;if(this.config.server.authToken)return this.config.server.authToken;let e=je.resolve(process.cwd(),".agent"),t=je.resolve(e,"token");if(A.existsSync(t)){if(A.lstatSync(t).isSymbolicLink())throw new Error("Token file must not be a symlink");Z(t);let o=le(t).trim();if(o)return o}let r=nr.randomBytes(24).toString("hex");if(A.existsSync(e)||(A.mkdirSync(e,{recursive:!0,mode:448}),Z(e,!0)),A.realpathSync(e)!==e)throw new Error("Token directory must not contain symlinks");try{let o=A.openSync(t,"wx",384);try{Z(t),A.writeFileSync(o,r),A.fsyncSync(o)}finally{A.closeSync(o)}}catch(o){if(o.code==="EEXIST")return this.getOrCreateAuthToken();throw o}return this.config.server.authToken=r,this.saveConfig(),r}registerProject(e,t){let r=A.realpathSync(je.resolve(t.root));if(!A.existsSync(r))throw new Error(`Project root directory does not exist: ${r}`);if(!A.statSync(r).isDirectory())throw new Error(`Project root must be a directory: ${r}`);if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(e))throw new Error("Project ID must contain letters, digits, dots, underscores or hyphens");for(let[n,s]of Object.entries(this.config.projects))if(n!==e&&A.existsSync(s.root)&&A.realpathSync(s.root)===r)throw new Error(`Project root is already registered as '${n}'`);this.config.projects[e]={...t,root:r},this.config.defaultProject||(this.config.defaultProject=e),this.saveConfig()}unregisterProject(e){return this.config.projects[e]?(delete this.config.projects[e],this.config.defaultProject===e&&(this.config.defaultProject=Object.keys(this.config.projects)[0]),this.saveConfig(),!0):!1}getProject(e){let t=e||this.config.defaultProject||Object.keys(this.config.projects)[0];if(!(!t||!this.config.projects[t]))return{id:t,config:this.config.projects[t]}}listProjects(){return this.config.projects}getPermission(e){return this.config.permissions[e]||"ask"}setAutoApprove(e){this.config.server.autoApprove=e}}});import zr from"node:os";import sr from"node:path";function xe(a){return sr.resolve(a||process.env.RYNSTER_OAUTH_STORAGE_PATH||sr.join(zr.homedir(),".rynster","oauth.json"))}var Je=j(()=>{"use strict";g(xe,"resolveOAuthStoragePath")});import X from"node:fs";import Br from"node:path";import ar from"node:crypto";import{z as b}from"zod";var T,Gr,Jr,Kr,qr,Yr,wt,ir,B,pe,P,ke=j(()=>{"use strict";ht();T=g(a=>ar.createHash("sha256").update(a).digest("hex"),"digest"),Gr={issuer:b.string(),resource:b.string(),clientId:b.string(),projectId:b.string(),projectKey:b.string(),scope:b.literal("mcp")},Jr=b.object({clientId:b.string(),clientName:b.string(),redirectUris:b.array(b.string()).min(1),createdAt:b.number(),issuer:b.string(),authMethod:b.enum(["none","client_secret_basic","client_secret_post","private_key_jwt"]),secretHash:b.string().optional()}),Kr=b.object({...Gr,id:b.string(),expiresAt:b.number(),revoked:b.boolean()}),qr=b.object({hash:b.string(),grantId:b.string(),expiresAt:b.number()}),Yr=b.object({hash:b.string(),grantId:b.string(),expiresAt:b.number(),used:b.boolean()}),wt=b.object({version:b.literal(2),clients:b.array(Jr),grants:b.array(Kr),access:b.array(qr),refresh:b.array(Yr),assertions:b.array(b.object({hash:b.string(),expiresAt:b.number()}))}).strict(),ir=g(()=>({version:2,clients:[],grants:[],access:[],refresh:[],assertions:[]}),"empty"),B=class extends Error{static{g(this,"PersistenceError")}},pe=class{constructor(e){this.file=e}file;static{g(this,"OAuthStore")}readSnapshot(){try{return wt.parse(JSON.parse(le(this.file)))}catch(e){if(e.code==="ENOENT")return ir();throw new B(`Cannot inspect OAuth store at ${this.file}; expected a readable regular version 2 file`)}}transact(e,t=!0){let r=Br.dirname(this.file),o,n;try{Qt(r),o=X.openSync(`${this.file}.lock`,"wx",384),X.writeFileSync(o,String(process.pid));let s=ir(),c;try{c=le(this.file)}catch(l){if(l.code!=="ENOENT")throw l}c!==void 0&&(s=wt.parse(JSON.parse(c)),Z(this.file));let i=e(s);if(t){let l=Date.now();s.grants=s.grants.filter(p=>p.expiresAt>l);let d=new Set(s.grants.map(p=>p.id));s.access=s.access.filter(p=>p.expiresAt>l&&d.has(p.grantId)),s.refresh=s.refresh.filter(p=>p.expiresAt>l&&d.has(p.grantId)),s.assertions=s.assertions.filter(p=>p.expiresAt>l),n=`${this.file}.${ar.randomUUID()}.tmp`;let u=X.openSync(n,"wx",384);try{Z(n),X.writeFileSync(u,JSON.stringify(wt.parse(s),null,2)),X.fsyncSync(u)}finally{X.closeSync(u)}X.renameSync(n,this.file),n=void 0,er(r)}return i}catch(s){throw s instanceof P?s:new B(`OAuth persistence failed at ${this.file}; check storage contents, permissions, and writer lock`)}finally{n&&X.unlinkSync(n),o!==void 0&&(X.closeSync(o),X.unlinkSync(`${this.file}.lock`))}}},P=class extends Error{constructor(t,r,o=400){super(r);this.error=t;this.status=o}error;status;static{g(this,"OAuthError")}}});import Vr from"node:https";import Zr from"node:dns/promises";import{BlockList as Xr,isIP as Qr}from"node:net";import{z as O}from"zod";import{createLocalJWKSet as eo,jwtVerify as to}from"jose";function ro(a){let e=Qr(a);return e===4?!de.check(a,"ipv4"):e===6&&/^[23][0-9a-f]{3}:/i.test(a)&&!de.check(a,"ipv6")}function vt(a){let e=new URL(a);if(e.protocol!=="https:"||e.username||e.password||e.hash||e.pathname==="/"||e.port&&e.port!=="443")throw new P("invalid_client","Client metadata must be an HTTPS document URL");return e}function cr(a){if(!a.keys.length||a.keys.some(e=>!["RSA","EC","OKP"].includes(e.kty??"")||["d","p","q","dp","dq","qi","oth","k"].some(t=>t in e)))throw new P("invalid_client","JWKS must contain public asymmetric keys only");return a}var de,St,oo,no,Ke,Pt=j(()=>{"use strict";ke();de=new Xr;for(let[a,e]of[["0.0.0.0",8],["10.0.0.0",8],["100.64.0.0",10],["127.0.0.0",8],["169.254.0.0",16],["172.16.0.0",12],["192.0.0.0",24],["192.0.2.0",24],["192.168.0.0",16],["192.88.99.0",24],["198.18.0.0",15],["198.51.100.0",24],["203.0.113.0",24],["224.0.0.0",3]])de.addSubnet(a,e);de.addSubnet("2001::",23,"ipv6");de.addSubnet("2001:db8::",32,"ipv6");de.addSubnet("2002::",16,"ipv6");de.addSubnet("3fff::",20,"ipv6");g(ro,"isPublicAddress");g(vt,"metadataUrl");St=O.string().max(2048).refine(a=>{try{let e=new URL(a);return!e.username&&!e.password&&!e.hash&&(e.protocol==="https:"||e.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(e.hostname))}catch{return!1}}),oo=O.object({client_id:O.string(),client_name:O.string().min(1).max(200),redirect_uris:O.array(St).min(1).max(20),token_endpoint_auth_method:O.enum(["none","private_key_jwt"]).default("none"),grant_types:O.array(O.string().min(1).max(200)).max(32).refine(a=>a.includes("authorization_code")).optional(),response_types:O.array(O.string().min(1).max(200)).max(32).refine(a=>a.includes("code")).optional(),jwks:O.object({keys:O.array(O.record(O.string(),O.unknown())).min(1).max(20)}).optional(),jwks_uri:O.string().optional()}).passthrough(),no=g(async a=>{let e=vt(a),t=e.hostname.replace(/^\[|\]$/g,""),r,o=await Promise.race([Zr.lookup(t,{all:!0}),new Promise((s,c)=>{r=setTimeout(()=>c(new Error("Metadata DNS lookup timed out")),5e3)})]).finally(()=>{r&&clearTimeout(r)});if(!o.length||o.some(({address:s})=>!ro(s)))throw new P("invalid_client","Metadata/JWKS resolves to a non-public address");let n=o[0];return new Promise((s,c)=>{let i=Vr.get(e,{headers:{Accept:"application/json","Accept-Encoding":"identity"},lookup:g((d,u,p)=>{typeof u=="object"&&u.all?p(null,[n]):p(null,n.address,n.family)},"lookup")},d=>{if(d.statusCode!==200||!/^application\/(?:[a-z0-9.+-]+\+)?json\b/i.test(d.headers["content-type"]||"")||d.headers["content-encoding"]&&d.headers["content-encoding"]!=="identity"){d.resume(),i.destroy(new Error("Metadata requires an unredirected JSON 200 response"));return}let u=0,p=[];d.on("data",f=>{u+=f.length,u>65536?i.destroy(new Error("Metadata exceeds 64 KiB")):p.push(f)}),d.on("error",c),d.on("end",()=>{try{let f=d.headers["cache-control"]||"",h=/no-store|no-cache/i.test(f)?0:Math.min(300,Number(f.match(/max-age=(\d+)/i)?.[1]??60));s({data:JSON.parse(Buffer.concat(p).toString("utf8")),maxAge:h})}catch(f){c(f)}})}),l=setTimeout(()=>i.destroy(new Error("Metadata fetch timed out")),5e3);i.on("close",()=>clearTimeout(l)),i.on("error",c)})},"fetchPublicJson");g(cr,"publicKeys");Ke=class{constructor(e=no){this.fetchJson=e}fetchJson;static{g(this,"ClientMetadata")}cache=new Map;async resolve(e,t){vt(e);let r=this.cache.get(e);if(r&&r.expiresAt>Date.now())return{...r,client:{...r.client,issuer:t}};try{let o=await this.fetchJson(e),n=oo.parse(o.data);if(n.client_id!==e||"client_secret"in n||"client_secret_expires_at"in n||n.jwks&&n.jwks_uri)throw new Error("Metadata identity or key material is invalid");let s,c=o.maxAge;if(n.token_endpoint_auth_method==="private_key_jwt")if(n.jwks)s=cr(n.jwks);else if(n.jwks_uri){vt(n.jwks_uri);let l=await this.fetchJson(n.jwks_uri),d=O.object({keys:O.array(O.record(O.string(),O.unknown())).min(1).max(20)}).parse(l.data);s=cr(d),c=Math.min(c,l.maxAge)}else throw new Error("private_key_jwt requires JWKS");let i={clientId:e,clientName:n.client_name,redirectUris:n.redirect_uris,issuer:t,authMethod:n.token_endpoint_auth_method,createdAt:Math.floor(Date.now()/1e3)};return this.cache.size>=100&&this.cache.delete(this.cache.keys().next().value),this.cache.set(e,{client:i,keys:s,expiresAt:Date.now()+Math.max(0,Math.min(c,300))*1e3}),{client:i,keys:s}}catch(o){throw new P("invalid_client",`Client metadata rejected: ${o instanceof Error?o.message:"invalid document"}`)}}async verifyAssertion(e,t,r,o,n="token"){let{keys:s}=await this.resolve(e,t);if(!s)throw new P("invalid_client","Client has no signing keys",401);try{let{payload:c}=await to(r,eo(s),{algorithms:["RS256","PS256","ES256","EdDSA"],issuer:e,subject:e,audience:`${t}/oauth/${n}`,requiredClaims:["exp","iat","jti"],maxTokenAge:"5m"});if(typeof c.jti!="string"||!c.jti||!c.exp||!c.iat||c.exp-c.iat>300)throw new Error("Assertion lifetime exceeds five minutes");let i=T(`${e}\0${c.jti}`);o.transact(l=>{if(l.assertions.some(d=>d.hash===i&&d.expiresAt>Date.now()))throw new P("invalid_client","Client assertion replay rejected",401);l.assertions.push({hash:i,expiresAt:c.exp*1e3})})}catch(c){throw c instanceof P||c instanceof B?c:new P("invalid_client","Invalid client assertion",401)}}}});import Ve from"node:crypto";import lr from"node:fs";function ue(a){let e=new URL(a);if(e.protocol!=="https:"&&!(e.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(e.hostname)))throw new Error("server.publicUrl must use HTTPS (HTTP is allowed only for loopback)");if(e.username||e.password||e.search||e.hash||e.pathname!=="/")throw new Error("server.publicUrl must be an origin without credentials, path, query or fragment");return e.origin}function pr(a,e,t){let r=a||e||t;return r?ue(r):void 0}function Ze(a,e){let t=lr.realpathSync(e),r=lr.statSync(t);return T(`root-v2\0${a}\0${t}\0${r.dev}:${r.ino}`)}function Ae(a){return a.replace(/[&<>"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}function $e(a,e){return Ve.timingSafeEqual(Buffer.from(T(a),"hex"),Buffer.from(T(e),"hex"))}var qe,Ye,so,Xe=j(()=>{"use strict";Je();Pt();ke();ke();g(ue,"normalizePublicUrl");g(pr,"effectivePublicUrl");g(Ze,"projectKey");g(Ae,"escapeHtml");g($e,"equalSecret");qe=g(a=>a+Ve.randomBytes(32).toString("base64url"),"random"),Ye=class{static{g(this,"OAuthManager")}store;metadata;publicUrl;localUrl;authCodes=new Map;consents=new Map;constructor(e,t,r){this.store=new pe(xe(e)),this.publicUrl=t?ue(t):void 0,this.metadata=new Ke(r)}setPublicUrl(e){this.publicUrl=e?ue(e):void 0}setLocalUrl(e){this.localUrl=ue(e)}getBaseUrl(e){if(this.publicUrl||this.localUrl)return this.publicUrl||this.localUrl;let t=ue(`http://${e.headers.host||"127.0.0.1:3333"}`);if(new URL(t).protocol!=="http:")throw new P("invalid_request","Configure a canonical public origin");return t}registerClient(e){return this.store.transact(t=>{t.clients=t.clients.filter(r=>r.clientId!==e.clientId),t.clients.push(e)}),e}async resolveClient(e,t){if(e.startsWith("https://"))return(await this.metadata.resolve(e,t)).client;let r=this.store.transact(o=>o.clients.find(n=>n.clientId===e&&n.issuer===t),!1);if(!r)throw new P("invalid_client","Unknown client for this issuer; register again");return r}validateAuthorizationRequest(e,t){if(e.responseType!=="code")throw new P("unsupported_response_type","response_type must be code");if(!t.redirectUris.includes(e.redirectUri))throw new P("invalid_request","Redirect URI must exactly match a registered URI");if(e.codeChallengeMethod!=="S256"||!/^[A-Za-z0-9_-]{43}$/.test(e.codeChallenge))throw new P("invalid_request","A valid S256 PKCE challenge is required");if(e.resource!==`${e.issuer}/mcp`)throw new P("invalid_target","Resource must match the canonical MCP URL");if(e.scope!=="mcp")throw new P("invalid_scope","Only the mcp scope is supported")}async beginConsent(e,t){let r=await this.resolveClient(e.clientId,e.issuer);this.validateAuthorizationRequest(e,r);for(let[n,s]of this.consents)s.expiresAt<=Date.now()&&this.consents.delete(n);if(this.consents.size>=1e3)throw new P("temporarily_unavailable","Too many pending authorizations",503);let o={...e,client:r,id:qe(""),browserHash:T(t),expiresAt:Date.now()+5*6e4};return this.consents.set(o.id,o),o}getConsent(e,t,r){let o=this.consents.get(e);if(!o||o.expiresAt<=Date.now()||o.browserHash!==T(t)||o.issuer!==r)throw new P("invalid_request","Consent expired or belongs to another browser/issuer",403);return o}async completeConsent(e,t){let r=await this.resolveClient(e.clientId,e.issuer);if(this.validateAuthorizationRequest(e,r),this.consents.get(e.id)!==e||e.expiresAt<=Date.now())throw new P("invalid_request","Consent already used or expired",403);if(this.consents.delete(e.id),!t)return;for(let[n,s]of this.authCodes)s.expiresAt<=Date.now()&&this.authCodes.delete(n);if(this.authCodes.size>=1e3)throw new P("temporarily_unavailable","Too many pending authorization codes",503);let o=qe("rynster_ac_");return this.authCodes.set(T(o),{...e,expiresAt:Date.now()+5*6e4}),o}issue(e,t){let r=qe("rynster_at_"),o=qe("rynster_rt_"),n=Math.min(Date.now()+36e5,t.expiresAt);return e.access.push({hash:T(r),grantId:t.id,expiresAt:n}),e.refresh.push({hash:T(o),grantId:t.id,expiresAt:t.expiresAt,used:!1}),{accessToken:r,refreshToken:o,expiresIn:Math.max(1,Math.floor((n-Date.now())/1e3))}}exchangeCodeForToken(e){let t=this.authCodes.get(T(e.code));if(!t||t.expiresAt<=Date.now()||t.clientId!==e.clientId||t.redirectUri!==e.redirectUri||t.issuer!==e.issuer||t.resource!==e.resource||!/^[A-Za-z0-9._~-]{43,128}$/.test(e.codeVerifier)||so(e.codeVerifier)!==t.codeChallenge)throw new P("invalid_grant","Code, redirect, PKCE, issuer or resource is invalid");let r=this.store.transact(o=>{let n={id:Ve.randomUUID(),clientId:t.clientId,issuer:t.issuer,resource:t.resource,scope:"mcp",projectId:t.projectId,projectKey:t.projectKey,expiresAt:Date.now()+2592e6,revoked:!1};return o.grants.push(n),this.issue(o,n)});return this.authCodes.delete(T(e.code)),r}refreshAccessToken(e){let t=this.store.transact(r=>{let o=r.refresh.find(s=>s.hash===T(e.refreshToken)),n=r.grants.find(s=>s.id===o?.grantId);if(!o||!n||o.expiresAt<=Date.now()||n.revoked||n.expiresAt<=Date.now()||n.clientId!==e.clientId||n.issuer!==e.issuer||n.resource!==e.resource)throw new P("invalid_grant","Invalid or expired refresh grant");if(o.used){n.revoked=!0;return}return o.used=!0,r.access=r.access.filter(s=>s.grantId!==n.id),this.issue(r,n)});if(!t)throw new P("invalid_grant","Refresh replay rejected; grant revoked");return t}revoke(e,t,r){this.store.transact(o=>{let n=[...o.access,...o.refresh].find(c=>c.hash===T(e)),s=o.grants.find(c=>c.id===n?.grantId&&c.clientId===t&&c.issuer===r);s&&(s.revoked=!0)})}getTokenInfo(e,t){return this.store.transact(r=>{let o=r.access.find(s=>s.hash===T(e)&&s.expiresAt>Date.now()),n=r.grants.find(s=>s.id===o?.grantId&&!s.revoked&&s.expiresAt>Date.now()&&s.issuer===t&&s.resource===`${t}/mcp`);return n&&o?{...n,accessExpiresAt:o.expiresAt}:void 0},!1)}renderConsentPage(e,t){let r=Ae(e.client.clientName),o=Ae(e.clientId);return`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
29
|
+
<title>Authorize Rynster Connection</title><style>body{font:16px system-ui;background:#0f172a;color:#f8fafc;margin:2rem auto;max-width:36rem;padding:1rem}input,button{display:block;margin:1rem 0;padding:.75rem}code{overflow-wrap:anywhere}.error{color:#fca5a5}</style></head><body>
|
|
30
|
+
<h1>Authorize Rynster Connection</h1><p>${r} requests access to project <strong>${Ae(e.projectId)}</strong>.</p>
|
|
31
|
+
<p>Client identity: <code>${o}</code>. The display name is supplied by the client.</p>
|
|
32
|
+
<p>Resource: <code>${Ae(e.resource)}</code><br>Scope: mcp. Local permission and approval rules apply.</p>
|
|
33
|
+
${t?`<p class="error">${Ae(t)}</p>`:""}
|
|
34
|
+
<form method="post" action="/oauth/authorize"><input type="hidden" name="transaction" value="${Ae(e.id)}">
|
|
35
|
+
<label for="owner_token">Rynster owner authorization token</label><p>Run <code>rynster token</code> in your local terminal to display it.</p>
|
|
36
|
+
<input type="password" id="owner_token" name="owner_token" autocomplete="off" required>
|
|
37
|
+
<button name="action" value="approve">Approve & Connect ${r}</button><button name="action" value="deny" formnovalidate>Deny</button>
|
|
38
|
+
</form></body></html>`}},so=g(a=>Ve.createHash("sha256").update(a).digest("base64url"),"digestChallenge")});var Qe,hr=j(()=>{Qe={name:"rynster",version:"0.1.0",description:"Provider-neutral local coding agent MCP server",bugs:{email:"baskar12ai@gmail.com"},type:"module",license:"SEE LICENSE IN LICENSE",funding:{type:"ko-fi",url:"https://ko-fi.com/bas12"},keywords:["mcp","ai","coding-agent","claude","chatgpt","local"],scripts:{prepack:"node ../../scripts/release.mjs prepack",prepublishOnly:"node ../../scripts/release.mjs publish-check"},publishConfig:{access:"public",registry:"https://registry.npmjs.org/"},engines:{node:">=22.17 <23 || >=24.2 <25"},main:"./dist/index.js",bin:{rynster:"./dist/index.js"},files:["dist/index.js","README.md","CHANGELOG.md","agent.config.example.json","LICENSE","SECURITY.md"],dependencies:{"@modelcontextprotocol/express":"^2.0.2","@modelcontextprotocol/node":"^2.1.1","@modelcontextprotocol/server":"^2.3.1","@vscode/ripgrep":"^1.18.0",commander:"^15.0.0",cors:"^2.8.6",diff:"^9.0.0",express:"^5.2.1",jose:"^6.2.12",zod:"^4.6.5"},devDependencies:{"@rynster/contracts":"*","@rynster/local-engine":"*","@rynster/mcp-adapter":"*"}}});var Ee,_e,bt,jt=j(()=>{"use strict";hr();Ee=Qe.name,_e=Qe.version,bt=Qe.funding.url});function G(){let a={CI:"true",TERM:"dumb",FORCE_COLOR:"0",NODE_ENV:"test"};for(let e of["PATH","HOME","USER","LANG","LC_ALL","TMPDIR","TMP","TEMP","SystemRoot","WINDIR","PATHEXT",...process.platform==="win32"?["ComSpec","USERPROFILE","APPDATA","LOCALAPPDATA","HOMEDRIVE","HOMEPATH"]:[]])process.env[e]&&(a[e]=process.env[e]);return a}var Ce=j(()=>{"use strict";g(G,"commandEnvironment")});import R from"node:fs";import W from"node:path";import{execFileSync as lo}from"node:child_process";var Me,xt=j(()=>{"use strict";Ce();Me=class{static{g(this,"ProjectManager")}configManager;constructor(e){this.configManager=e}getProject(e){let t=this.configManager.getProject(e);if(!t){let o=Object.keys(this.configManager.listProjects());throw o.length===0?new Error("No projects registered. Register a project first using 'coding-agent-mcp register <name> <path>' or in agent.config.json"):new Error(`Project '${e}' not found. Available registered projects: ${o.join(", ")}`)}if(!R.existsSync(t.config.root))throw new Error(`Registered project root directory does not exist: ${t.config.root}`);let r=R.realpathSync(t.config.root);for(let[o,n]of Object.entries(this.configManager.listProjects()))if(o!==t.id&&R.existsSync(n.root)&&R.realpathSync(n.root)===r)throw new Error("Duplicate project roots; remove aliases from configuration");return{id:t.id,config:t.config,root:R.realpathSync(t.config.root)}}listProjects(){return this.configManager.listProjects()}getProjectInfo(e){let{id:t,config:r,root:o}=this.getProject(e),n=this.detectFramework(o),s=this.detectPackageManager(o),{branch:c,hasGit:i}=this.detectGitInfo(o);return{name:t,root:o,framework:n,packageManager:s,branch:c,description:r.description,hasGit:i,commands:r.commands}}detectFramework(e){let t=W.join(e,"package.json");if(R.existsSync(t))try{let r=JSON.parse(R.readFileSync(t,"utf-8")),o={...r.dependencies,...r.devDependencies};return o["react-native"]?"react-native":o.next?"nextjs":o["@nestjs/core"]?"nestjs":o.vue||o.nuxt?"vue":o.svelte?"svelte":o["@angular/core"]?"angular":o.react?"react":o.express?"express":"node"}catch{return"node"}return R.existsSync(W.join(e,"Cargo.toml"))?"rust":R.existsSync(W.join(e,"go.mod"))?"go":R.existsSync(W.join(e,"requirements.txt"))||R.existsSync(W.join(e,"pyproject.toml"))||R.existsSync(W.join(e,"setup.py"))?"python":"unknown"}detectPackageManager(e){let t=W.join(e,"package.json");if(R.existsSync(t))try{let r=JSON.parse(R.readFileSync(t,"utf-8"));if(r.packageManager)return r.packageManager.split("@")[0]}catch{}return R.existsSync(W.join(e,"pnpm-lock.yaml"))?"pnpm":R.existsSync(W.join(e,"yarn.lock"))?"yarn":R.existsSync(W.join(e,"bun.lockb"))||R.existsSync(W.join(e,"bun.lock"))?"bun":R.existsSync(W.join(e,"package-lock.json"))?"npm":R.existsSync(W.join(e,"Cargo.lock"))?"cargo":R.existsSync(W.join(e,"poetry.lock"))?"poetry":"npm"}detectGitInfo(e){let t=W.join(e,".git");if(!R.existsSync(t))return{branch:"none",hasGit:!1};try{return{branch:lo("git",["-c","core.fsmonitor=false","rev-parse","--abbrev-ref","HEAD"],{env:G(),timeout:15e3,cwd:e,stdio:["ignore","pipe","ignore"],encoding:"utf-8"}).trim()||"HEAD",hasGit:!0}}catch{return{branch:"unknown",hasGit:!0}}}}});import Re from"node:path";import fr from"node:fs";import{AsyncLocalStorage as po}from"node:async_hooks";var I,ge=j(()=>{"use strict";he();I=class{static{g(this,"SecretGuard")}static scope=new po;static withProtectedPaths(e,t){return this.scope.run(e.map(r=>Re.resolve(r)),t)}static isProtectedAbsolute(e){let t=Re.resolve(e),r=t;try{r=fr.realpathSync(t)}catch{}return(this.scope.getStore()||[]).some(o=>{let n=o;try{n=fr.realpathSync(o)}catch{}return[o,n].some(s=>[t,r].some(c=>Re.matchesGlob(c,s)||c===s||c.startsWith(s+Re.sep)||c.startsWith(s+".")))})}static assertSafeFile(e,t){if(this.isProtectedAbsolute(Re.resolve(e,t))||this.isBlockedPath(t))throw new $("Access denied: protected credential or configuration path")}static PROTECTED_FILE_PATTERNS=[/^\.env(\..+)?$/i,/^agent\.config\.json(?:\..+)?$/i,/^\.(npmrc|netrc|pypirc)$/i,/\.pem$/i,/\.key$/i,/\.p12$/i,/\.pfx$/i,/\.jks$/i,/\.keystore$/i,/^credentials(\..+)?$/i,/^service-account.*\.json$/i,/^id_rsa/i,/^id_ed25519/i,/\.asc$/i];static BLOCKED_DIRECTORIES=new Set([".git","node_modules",".gradle","Pods",".agent",".rynster",".aws",".ssh",".codex",".agents",".idea",".vscode"]);static IGNORED_DIRECTORIES=new Set([".git","node_modules",".gradle","Pods",".agent",".rynster",".aws",".ssh",".codex",".agents","build","dist","out",".next",".nuxt",".turbo",".cache",".expo","coverage",".idea",".vscode"]);static isProtectedFile(e){let t=Re.basename(e);return this.PROTECTED_FILE_PATTERNS.some(r=>r.test(t))}static isBlockedDirectory(e){return e.split(/[/\\]/).some(r=>this.BLOCKED_DIRECTORIES.has(r))}static isBlockedPath(e){let t=e.split(/[/\\]/);for(let r of t)if(this.BLOCKED_DIRECTORIES.has(r))return!0;return this.isProtectedFile(e)}static shouldIgnoreInTree(e){return this.IGNORED_DIRECTORIES.has(e)}static assertFileReadable(e){if(this.isBlockedPath(e))throw new $(`Access Denied: '${e}' is a protected secret or directory. File contents cannot be accessed.`)}static assertFileWritable(e){if(this.isBlockedPath(e))throw new $(`Write Denied: '${e}' is a protected secret or directory and cannot be modified.`)}}});import M from"node:path";import Te from"node:fs";var $,D,he=j(()=>{"use strict";ge();$=class extends Error{static{g(this,"SecurityViolationError")}constructor(e){super(e),this.name="SecurityViolationError"}},D=class{static{g(this,"PathGuard")}static resolveSafePath(e,t,r=!0){if(!e)throw new $("Project root is required for path validation");if(!t||t.trim()==="")throw new $("Target path cannot be empty");if(process.platform==="win32"&&t.slice(M.parse(t).root.length).includes(":"))throw new $("Windows alternate data streams are unsupported");let o;try{o=Te.realpathSync(M.resolve(e))}catch{throw new $("Project root does not exist or cannot be resolved")}I.assertSafeFile(o,t);let n=M.normalize(t);if(t.split(/[/\\]/).includes("..")){let d=M.resolve(o,n);if(!this.isSubpath(o,d))throw new $(`Directory traversal detected: '${t}' escapes project root`)}let c;M.isAbsolute(n)?c=M.normalize(n):c=M.resolve(o,n);let i=this.resolveSymlinks(c,r);if(!this.isSubpath(o,i))throw new $(`Access denied: '${t}' resolves to '${i}', outside project root '${o}'`);I.assertSafeFile(o,M.relative(o,i));let l=M.relative(o,i);return{absolutePath:i,relativePath:l||"."}}static resolveSymlinks(e,t){if(Te.existsSync(e))try{return Te.realpathSync(e)}catch(n){throw new $(`Failed to resolve symlink for '${e}': ${n}`)}if(!t)throw new $(`Path does not exist: '${e}'`);let r=M.dirname(e),o=[M.basename(e)];for(;r!==M.dirname(r);){if(Te.existsSync(r))try{let n=Te.realpathSync(r);return M.resolve(n,...o)}catch(n){throw new $(`Failed to resolve ancestor symlink for '${r}': ${n}`)}o.unshift(M.basename(r)),r=M.dirname(r)}return M.normalize(e)}static isSubpath(e,t){let r=M.normalize(e),o=M.normalize(t);if(o===r)return!0;let n=M.relative(r,o);return!n.startsWith("..")&&!M.isAbsolute(n)}}});import z from"node:fs";import et from"node:path";import uo from"node:crypto";var oe,tt=j(()=>{"use strict";he();ge();oe=class{static{g(this,"ProjectFilesystem")}static readDescriptor(e){let t=z.statSync(e),r=z.openSync(e,z.constants.O_RDONLY|(z.constants.O_NOFOLLOW||0));try{let o=z.fstatSync(r);if(!o.isFile()||o.nlink!==1||o.dev!==t.dev||o.ino!==t.ino)throw new Error("Unsafe or changed file identity");return{content:z.readFileSync(r,"utf8"),stat:o}}finally{z.closeSync(r)}}static getTree(e,t=".",r={}){let{absolutePath:o,relativePath:n}=D.resolveSafePath(e,t);if(!z.existsSync(o))throw new Error(`Path does not exist: ${t}`);if(!z.statSync(o).isDirectory())return{tree:`${n}
|
|
39
|
+
`,entryCount:1,truncated:!1};let c=r.depth!==void 0?Math.max(1,r.depth):3,i=r.maxEntries||500,l=0,d=!1,u=[],p=n==="."?et.basename(e)+"/":n+"/";u.push(p);let f=g((h,m,v)=>{if(m>c||l>=i){l>=i&&(d=!0);return}let w;try{w=z.readdirSync(h,{withFileTypes:!0})}catch{return}let S=w.filter(x=>!(I.shouldIgnoreInTree(x.name)||I.isBlockedPath(x.name)||I.isProtectedAbsolute(et.join(h,x.name))||x.isSymbolicLink()));S.sort((x,_)=>x.isDirectory()&&!_.isDirectory()?-1:!x.isDirectory()&&_.isDirectory()?1:x.name.localeCompare(_.name));for(let x=0;x<S.length;x++){if(l>=i){d=!0;return}let _=S[x],ee=x===S.length-1,te=et.join(h,_.name),F=ee?"\u2514\u2500\u2500 ":"\u251C\u2500\u2500 ",H=v+(ee?" ":"\u2502 ");l++,_.isDirectory()?(u.push(`${v}${F}${_.name}/`),f(te,m+1,H)):u.push(`${v}${F}${_.name}`)}},"traverse");return f(o,1,""),d&&u.push(`
|
|
40
|
+
... [Truncated: reached max limit of ${i} entries]`),{tree:u.join(`
|
|
41
|
+
`),entryCount:l,truncated:d}}static readFile(e,t,r,o){let{absolutePath:n,relativePath:s}=D.resolveSafePath(e,t);if(I.assertFileReadable(s),!z.existsSync(n))throw new Error(`File does not exist: ${t}`);let c=z.statSync(n);if(c.isDirectory())throw new Error(`Cannot read directory as file: ${t}`);let i=this.readDescriptor(n).content,l=this.calculateHash(i),d=i.split(/\r?\n/),u=d.length,p=Math.max(1,r||1),f=o!==void 0?Math.min(u,o):u;return p>u?{content:`(startLine ${p} is beyond total lines ${u})`,totalLines:u,startLine:p,endLine:f,language:this.detectLanguage(t),modifiedTime:c.mtime.toISOString(),revision:l,relativePath:s}:{content:d.slice(p-1,f).map((v,w)=>`${(p+w).toString().padStart(4," ")} | ${v}`).join(`
|
|
42
|
+
`),totalLines:u,startLine:p,endLine:f,language:this.detectLanguage(t),modifiedTime:c.mtime.toISOString(),revision:l,relativePath:s}}static readRaw(e,t){let{absolutePath:r,relativePath:o}=D.resolveSafePath(e,t);if(I.assertFileReadable(o),!z.existsSync(r))throw new Error(`File does not exist: ${t}`);let n=this.readDescriptor(r).content,s=this.calculateHash(n);return{content:n,revision:s}}static calculateHash(e){return uo.createHash("sha256").update(e,"utf-8").digest("hex")}static detectLanguage(e){switch(et.extname(e).toLowerCase()){case".ts":case".mts":case".cts":return"typescript";case".tsx":return"typescriptreact";case".js":case".mjs":case".cjs":return"javascript";case".jsx":return"javascriptreact";case".json":return"json";case".md":return"markdown";case".py":return"python";case".rs":return"rust";case".go":return"go";case".java":return"java";case".kt":return"kotlin";case".swift":return"swift";case".cpp":case".c":case".h":return"c_cpp";case".css":case".scss":case".sass":return"css";case".html":return"html";case".yaml":case".yml":return"yaml";case".sh":case".bash":return"shell";case".sql":return"sql";default:return"text"}}}});import{execFile as go}from"node:child_process";import ne from"node:fs";import kt from"node:path";import{promisify as ho}from"node:util";var mr,Le,At=j(()=>{"use strict";he();ge();Ce();mr=ho(go),Le=class{static{g(this,"ProjectSearch")}static rgBinaryPath=null;static async getRipgrepBinary(){if(this.rgBinaryPath!==null)return this.rgBinaryPath;try{let e=await import("@vscode/ripgrep");if(e.rgPath&&ne.existsSync(e.rgPath))return this.rgBinaryPath=e.rgPath,this.rgBinaryPath}catch{}try{let{stdout:e}=await mr("which",["rg"]),t=e.trim();if(t&&ne.existsSync(t))return this.rgBinaryPath=t,this.rgBinaryPath}catch{}return null}static async search(e,t,r={}){if(!t||t.trim()==="")return[];let o=r.path||".",{absolutePath:n,relativePath:s}=D.resolveSafePath(e,o);I.assertFileReadable(s);let c=r.maxResults||30,i=await this.getRipgrepBinary();return i?this.searchWithRipgrep(e,n,i,t,r,c):this.searchWithNode(e,n,t,r,c)}static async searchWithRipgrep(e,t,r,o,n,s){let c=["--json","--no-config","-n","--no-heading","--color","never","--max-count",String(s),"--glob","!node_modules/**","--glob","!.git/**","--glob","!.agent/**","--glob","!dist/**","--glob","!build/**","--glob","!Pods/**","--glob","!.gradle/**","--glob","!.env*","--glob","!*.pem","--glob","!*.key","--glob","!credentials*"];n.caseSensitive||c.push("-i"),n.regex||c.push("-F"),c.push("-e",o,"--",t);try{let{stdout:i}=await mr(r,c,{env:G(),maxBuffer:10485760,timeout:1e4});return this.parseRipgrepOutput(e,i,s)}catch(i){return i.code===1&&i.stdout?this.parseRipgrepOutput(e,i.stdout,s):i.code===1?[]:this.searchWithNode(e,t,o,n,s)}}static parseRipgrepOutput(e,t,r){let o=t.split(/\r?\n/).filter(s=>s.trim().length>0),n=[];for(let s of o){if(n.length>=r)break;let c;try{c=JSON.parse(s)}catch{continue}if(c.type!=="match"||!c.data.path.text||!c.data.lines.text)continue;let i=c.data.path.text;try{if(ne.lstatSync(i).nlink!==1)continue}catch{continue}let l=c.data.line_number,d=c.data.lines.text.trim(),u;try{u=D.resolveSafePath(e,i,!1).relativePath}catch{continue}n.push({path:u,line:l,snippet:d.length>300?d.slice(0,300)+"...":d})}return n}static async searchWithNode(e,t,r,o,n){let s=[],c=o.caseSensitive?r:r.toLowerCase(),i=g(l=>{if(s.length>=n)return;let d;try{d=ne.readdirSync(l,{withFileTypes:!0})}catch{return}for(let u of d){if(s.length>=n)return;let p=kt.join(l,u.name),f=kt.relative(e,p);if(u.isDirectory()){if(I.shouldIgnoreInTree(u.name))continue;i(p)}else if(u.isFile()){if(I.isBlockedPath(f)||I.isProtectedAbsolute(p))continue;try{let h=ne.statSync(p);if(h.nlink!==1||h.size>2*1024*1024)continue;let v=ne.readFileSync(p,"utf-8").split(/\r?\n/);for(let w=0;w<v.length;w++){if(s.length>=n)return;let S=v[w];(o.caseSensitive?S.includes(c):S.toLowerCase().includes(c))&&s.push({path:f,line:w+1,snippet:S.trim().slice(0,300)})}}catch{}}}},"walk");if(ne.statSync(t).isFile()){let l=ne.lstatSync(t);if(l.isSymbolicLink()||l.nlink!==1||I.isProtectedAbsolute(t))return[];let d=ne.readFileSync(t,"utf8").split(/\r?\n/);for(let u=0;u<d.length&&s.length<n;u++)(o.caseSensitive?d[u]:d[u].toLowerCase()).includes(c)&&s.push({path:kt.relative(e,t),line:u+1,snippet:d[u].trim().slice(0,300)})}else i(t);return s}}});import{execFile as fo}from"node:child_process";import rt from"node:fs";import mo from"node:crypto";import yr from"node:path";import{promisify as yo}from"node:util";var wo,Q,ot=j(()=>{"use strict";he();Ce();wo=yo(fo),Q=class{static{g(this,"GitManager")}static async runGit(e,t){let r=yr.join(e,".git");if(!rt.existsSync(r)&&!rt.existsSync(yr.join(e,"../.git")))throw new Error(`Directory is not a git repository: ${e}`);try{let{stdout:o}=await wo("git",["-c","core.fsmonitor=false","-c","core.hooksPath=/dev/null","-c","core.pager=cat","--no-pager",...t],{env:G(),cwd:e,maxBuffer:5242880,timeout:15e3});return o}catch{throw new Error("Git operation failed")}}static async getStatus(e){let t="HEAD";try{t=(await this.runGit(e,["rev-parse","--abbrev-ref","HEAD"])).trim()}catch{}let r=await this.runGit(e,["status","--porcelain","-u"]),o=[],n=[],s=[],c=r.split(/\r?\n/).filter(i=>i.trim().length>0);for(let i of c){let l=i[0],d=i[1],u=i.slice(3).trim();l==="?"&&d==="?"?s.push(u):(l!==" "&&l!=="?"&&o.push(u),d!==" "&&d!=="?"&&n.push(u))}return{branch:t,isClean:c.length===0,staged:o,modified:n,untracked:s}}static async getDiff(e,t={}){let r=t.base?await this.resolveBase(e,t.base):void 0,o=["diff","--no-ext-diff","--no-textconv"];t.staged&&o.push("--staged"),r&&o.push(r);let n=t.path?D.resolveSafePath(e,t.path).relativePath:void 0,s=(await this.runGit(e,[...o,"--name-only","-z","--",...n?[`:(literal)${n}`]:[]])).split("\0").filter(Boolean).filter(c=>{try{return D.resolveSafePath(e,c),!0}catch{return!1}});return s.length?this.runGit(e,[...o,"--",...s.map(c=>`:(literal)${c}`)]):""}static async resolveBase(e,t){if(!t||t.startsWith("-")||t.includes("\0"))throw new Error("Invalid Git base revision");let r=(await this.runGit(e,["rev-parse","--verify","--end-of-options",`${t}^{commit}`])).trim();if(!/^[0-9a-f]{40,64}$/.test(r))throw new Error("Invalid Git base revision");return r}static async getOperationSnapshot(e){let t=mo.createHash("sha256");t.update(await this.runGit(e,["status","--porcelain","-z"])),t.update(await this.runGit(e,["rev-parse","HEAD"]));let r=(await this.runGit(e,["ls-files","-z","--cached","--others","--exclude-standard"])).split("\0").filter(Boolean);for(let o of[...new Set(r)].sort()){let n=D.resolveSafePath(e,o).absolutePath;t.update(o),t.update(rt.existsSync(n)?rt.readFileSync(n):"absent")}return t.digest("hex")}static async commit(e,t,r=()=>{}){if(!t||t.trim()==="")throw new Error("Commit message cannot be empty");let o=(await this.runGit(e,["ls-files","-z","--cached","--others","--exclude-standard"])).split("\0").filter(Boolean);for(let s of o)D.resolveSafePath(e,s);return r(),await this.runGit(e,["add","-A"]),r(),(await this.runGit(e,["commit","-m",t])).trim()}}});import{execFile as vo}from"node:child_process";import ae from"node:fs";import fe from"node:path";import{promisify as So}from"node:util";var Et,me,_t=j(()=>{"use strict";Ce();Et=So(vo),me=class{static{g(this,"WorktreeManager")}static async createWorktree(e,t,r){if(!/^[a-zA-Z0-9_-]+$/.test(t)||r?.startsWith("-"))throw new Error("Invalid worktree parameters");let o=fe.join(e,".git");if(!ae.existsSync(o))throw new Error(`Project root is not a git repository: ${e}`);if(!/^[a-zA-Z0-9_-]+$/.test(t))throw new Error("Invalid worktree session ID");let n=fe.join(".agent","worktrees",t),s=fe.resolve(e,n),c=`agent/${t}`,i=fe.dirname(s);for(let d of[fe.join(e,".agent"),i])if(ae.existsSync(d)){let u=ae.lstatSync(d);if(!u.isDirectory()||u.isSymbolicLink())throw new Error("Worktree parent must not contain symlinks")}else ae.mkdirSync(d,{mode:448});if(ae.existsSync(s))throw new Error("Worktree destination already exists");if(ae.realpathSync(i)!==i)throw new Error("Worktree parent must not contain symlinks");let l=["worktree","add","-b",c,n];r&&l.push(r);try{await Et("git",["-c","core.hooksPath=/dev/null",...l],{cwd:e,env:G(),timeout:15e3})}catch{throw new Error("Failed to create git worktree")}return{sessionId:t,worktreePath:s,branch:c,exists:!0}}static async removeWorktree(e,t,r=!1,o=()=>{}){if(!/^[a-zA-Z0-9_-]+$/.test(t))throw new Error("Invalid worktree session ID");let n=fe.join(".agent","worktrees",t),s=`agent/${t}`;try{o(),await Et("git",["-c","core.hooksPath=/dev/null","worktree","remove","--force",n],{cwd:e,env:G(),timeout:15e3})}catch{throw new Error("Worktree cleanup failed; session retained for recovery")}if(r)try{o(),await Et("git",["-c","core.hooksPath=/dev/null","branch","-D",s],{cwd:e,env:G(),timeout:15e3})}catch{throw new Error("Worktree removed but branch cleanup failed; inspect the branch before retrying")}}static isWorktree(e){let t=fe.join(e,".git");return ae.existsSync(t)&&ae.statSync(t).isFile()}}});import Po from"node:crypto";import Ct from"node:fs";import{AsyncLocalStorage as bo}from"node:async_hooks";var De,Rt=j(()=>{"use strict";_t();ot();De=class{static{g(this,"SessionManager")}sessions=new Map;defaults=new Map;context=new bo;runWithContext(e,t){return this.context.run(e,t)}getOperationContext(){return this.context.getStore()||{}}assertAuthorized(){this.context.getStore()?.signal?.throwIfAborted(),this.context.getStore()?.assertAuthorized?.()}get ownerId(){return this.context.getStore()?.ownerId||"local"}defaultKey(e){return JSON.stringify([this.ownerId,e])}rootIdentity(e){let t=Ct.statSync(e);return`${t.dev}:${t.ino}`}assertSession(e){if(this.assertAuthorized(),e.ownerId!==this.ownerId)throw new Error("Session is owned by another caller");if(this.projectManager.getProject(e.projectId).root!==e.projectRoot||this.rootIdentity(e.projectRoot)!==e.projectRootIdentity)throw new Error("Session project root changed; start a new session")}projectManager;permissionEngine;auditLogger;constructor(e,t,r){this.projectManager=e,this.permissionEngine=t,this.auditLogger=r}generateSessionId(){return Po.randomUUID()}async createSession(e){let{id:t,root:r}=this.projectManager.getProject(e.projectId),o=this.generateSessionId(),n;if(e.useWorktree)try{n=(await me.createWorktree(r,o,e.baseBranch)).worktreePath}catch(c){throw new Error(`Requested worktree isolation failed: ${c instanceof Error?c.message:"creation failed"}`)}let s={id:o,projectId:t,ownerId:this.ownerId,projectRoot:r,projectRootIdentity:this.rootIdentity(r),startedAt:new Date,lastActiveAt:new Date,permissions:new Map,filesRead:new Set,filesChanged:new Set,fileRevisions:new Map,worktree:n,allowAllWrites:!1};return this.sessions.set(o,s),this.defaults.set(this.defaultKey(t),o),this.auditLogger.log({session:o,project:t,tool:"session_start",status:"success",metadata:{useWorktree:e.useWorktree,worktree:n}}),s}getOrCreateSession(e,t){let r=this.projectManager.getProject(e);if(t){let i=this.sessions.get(t);if(!i)throw new Error(`Session ${t} not found`);if(this.assertSession(i),i.projectId!==r.id)throw new Error(`Session project mismatch: session belongs to ${i.projectId}, not ${r.id}`);return i.lastActiveAt=new Date,i}let o=this.defaults.get(this.defaultKey(r.id));if(o&&this.sessions.has(o)){let i=this.sessions.get(o);if(this.assertSession(i),i.projectId===r.id)return i.lastActiveAt=new Date,i}let n=this.projectManager.getProject(e),s=this.generateSessionId(),c={id:s,projectId:n.id,ownerId:this.ownerId,projectRoot:n.root,projectRootIdentity:this.rootIdentity(n.root),startedAt:new Date,lastActiveAt:new Date,permissions:new Map,filesRead:new Set,filesChanged:new Set,fileRevisions:new Map,allowAllWrites:!1};return this.sessions.set(s,c),this.defaults.set(this.defaultKey(n.id),s),c}getSession(e){let t=this.sessions.get(e);return t&&this.assertSession(t),t}listSessions(){return Array.from(this.sessions.values()).filter(e=>e.ownerId===this.ownerId)}getWorkingDir(e,t){if(e&&this.assertSession(e),e?.worktree){let o=Ct.realpathSync(e.worktree);if(o!==e.worktree||!Ct.statSync(o).isDirectory())throw new Error("Worktree path changed");return o}if(e){let{root:o}=this.projectManager.getProject(e.projectId);return o}if(t)return t;let{root:r}=this.projectManager.getProject();return r}recordRead(e,t,r){e.filesRead.add(t),e.fileRevisions.set(t,r),e.lastActiveAt=new Date}recordChanged(e,t,r){e.filesChanged.add(t),e.fileRevisions.set(t,r),e.lastActiveAt=new Date}async stopSession(e,t="keep",r){let o=this.sessions.get(e);if(!o)throw new Error(`Session ${e} not found`);this.assertSession(o);let n;if(o.worktree){let{root:s}=this.projectManager.getProject(o.projectId);if(t==="commit"){let c=r||`agent(${e}): apply changes`;n=await Q.commit(o.worktree,c,()=>this.assertAuthorized()),await me.removeWorktree(s,e,!1,()=>this.assertAuthorized())}else t==="discard"&&await me.removeWorktree(s,e,!0,()=>this.assertAuthorized())}this.sessions.delete(e);for(let[s,c]of this.defaults)c===e&&this.defaults.delete(s);return this.auditLogger.log({session:e,project:o.projectId,tool:"session_stop",status:"success",metadata:{action:t,commit:n}}),{status:`Session ${e} stopped with action: ${t}`,commit:n}}}});import E from"node:fs";import se from"node:path";import jo from"node:crypto";function nt(a,e){return a.dev===e.dev&&a.ino===e.ino}function It(a,e){let t=new Map,r=se.relative(a,se.dirname(e)),o=a;for(let n of["",...r.split(se.sep).filter(Boolean)]){n&&(o=se.join(o,n));try{let s=E.lstatSync(o);if(!s.isDirectory()||s.isSymbolicLink())throw new $("Patch directories must not be symlinks");t.set(o,{dev:s.dev,ino:s.ino})}catch(s){if(s.code==="ENOENT")break;throw s}}return t}function wr(a){let e=E.openSync(a,E.constants.O_RDONLY|(E.constants.O_NOFOLLOW||0));try{let t=E.fstatSync(e);if(!t.isFile()||t.nlink!==1)throw new $("Patch target must be a regular file without hard links");return{content:E.readFileSync(e,"utf8"),stat:t}}finally{E.closeSync(e)}}function vr(a,e){let t=E.realpathSync(a),{absolutePath:r,relativePath:o}=D.resolveSafePath(a,e);if(se.resolve(t,e)!==r)throw new $("Patch paths must not contain symlinks");let n=It(t,r);try{let{content:s,stat:c}=wr(r);return{root:t,target:r,relative:o,content:s,exists:!0,mode:c.mode&511,identity:{dev:c.dev,ino:c.ino},directories:n}}catch(s){if(s.code!=="ENOENT")throw s;return{root:t,target:r,relative:o,content:"",exists:!1,mode:384,directories:n}}}function st(a){if(D.resolveSafePath(a.root,a.relative).absolutePath!==a.target)throw new Y("Patch target path changed during approval");let t=It(a.root,a.target);for(let[r,o]of a.directories)if(!t.has(r)||!nt(o,t.get(r)))throw new Y("Patch directory changed during approval");try{let r=wr(a.target);if(!a.exists||!a.identity||!nt(r.stat,a.identity)||r.content!==a.content)throw new Y("File changed during approval; re-read and retry")}catch(r){if(r.code==="ENOENT"&&!a.exists)return;throw r.code==="ENOENT"?new Y("File disappeared during approval"):r}}function Sr(a,e){st(a);let t=se.dirname(a.target);E.mkdirSync(t,{recursive:!0});let r=It(a.root,a.target),o=E.openSync(t,E.constants.O_RDONLY|(E.constants.O_DIRECTORY||0)|(E.constants.O_NOFOLLOW||0)),n;try{if(!nt(E.fstatSync(o),r.get(t)))throw new Y("Patch parent changed");let s=`/proc/self/fd/${o}`,c=process.platform==="linux"&&E.existsSync(s)?s:t,i=se.join(c,se.basename(a.target));n=se.join(c,`.rynster-patch-${jo.randomUUID()}.tmp`);let l=E.openSync(n,E.constants.O_WRONLY|E.constants.O_CREAT|E.constants.O_EXCL|(E.constants.O_NOFOLLOW||0),a.mode);try{E.writeFileSync(l,e,"utf8"),E.fsyncSync(l)}finally{E.closeSync(l)}if(st(a),!nt(E.statSync(t),E.fstatSync(o)))throw new Y("Patch parent changed before commit");E.renameSync(n,i),n=void 0}finally{if(n)try{E.unlinkSync(n)}catch{}E.closeSync(o)}}var Y,Pr=j(()=>{"use strict";he();Y=class extends Error{static{g(this,"FileChangedError")}};g(nt,"same");g(It,"directories");g(wr,"readTarget");g(vr,"snapshotWrite");g(st,"verifyWrite");g(Sr,"commitWrite")});import br from"node:crypto";async function ce(a,e,t){e.assertAuthorized();let r=e.getOperationContext(),o=Date.now()+5*6e4,n=br.createHash("sha256").update(JSON.stringify({action:t.action,projectId:t.projectId,projectRoot:t.projectRoot,sessionId:t.sessionId,targetPath:t.targetPath,revision:t.revision,summary:t.summary,diff:t.diff})).digest("hex"),s=r.signal||t.signal;s?.throwIfAborted();let c,i;try{let l=await Promise.race([a.requestApproval({...t,operationId:br.randomUUID(),operationDigest:n,ownerId:r.ownerId||"local",expiresAt:o,signal:s}),new Promise((d,u)=>{c=setTimeout(()=>u(new Error("Approval expired; retry the operation")),Math.max(0,o-Date.now())),i=g(()=>u(new Error("Operation canceled while awaiting approval")),"abort"),s?.addEventListener("abort",i,{once:!0}),s?.aborted&&i()})]);if(Date.now()>=o)throw new Error("Approval expired; retry the operation");return e.assertAuthorized(),s?.throwIfAborted(),l}finally{c&&clearTimeout(c),i&&s?.removeEventListener("abort",i)}}var it=j(()=>{"use strict";g(ce,"requestOperationApproval")});import*as ye from"diff";var Ne,Ot=j(()=>{"use strict";Pr();it();ge();tt();Ne=class{static{g(this,"SafePatcher")}approvalService;sessionManager;auditLogger;permissionEngine;constructor(e,t,r,o){this.approvalService=e,this.sessionManager=t,this.auditLogger=r,this.permissionEngine=o}async applyPatch(e){let t=this.sessionManager.getOrCreateSession(e.projectId,e.sessionId),r=this.sessionManager.getWorkingDir(t),o=vr(r,e.path),{relative:n}=o;I.assertFileWritable(n);let c=o.exists?"project.write":"project.create",i=this.permissionEngine.check(c,{allowAllWrites:t.allowAllWrites});if(i==="deny")return this.auditLogger.log({session:t.id,project:t.projectId,tool:"project_patch",status:"denied",metadata:{path:n,permission:c}}),{success:!1,status:"ERROR",filePath:n,addedLines:0,removedLines:0,diff:"",message:`Permission denied: ${c} is set to DENY`};let l=o.content,d=oe.calculateHash(l);if(e.expectedRevision&&e.expectedRevision!==d)return this.auditLogger.log({session:t.id,project:t.projectId,tool:"project_patch",status:"error",metadata:{path:n,error:"FILE_CHANGED",currentRevision:d,expectedRevision:e.expectedRevision}}),{success:!1,status:"FILE_CHANGED",filePath:n,addedLines:0,removedLines:0,diff:"",message:`FILE_CHANGED: The file '${n}' has been modified since it was read. Current revision is '${d}', but expected revision was '${e.expectedRevision}'. Please re-read the file and retry your patch.`};let u;try{u=this.computePatchedContent(l,e.patch)}catch(S){return{success:!1,status:"ERROR",filePath:n,addedLines:0,removedLines:0,diff:"",message:`Failed to parse or apply patch: ${S.message}`}}let p=ye.createTwoFilesPatch(n,n,l,u,"current","patched"),f=ye.diffLines(l,u),h=0,m=0;for(let S of f)S.added&&(h+=S.count||0),S.removed&&(m+=S.count||0);let v=!1;if(i==="ask"&&!t.allowAllWrites){let S=await ce(this.approvalService,this.sessionManager,{sessionId:t.id,projectId:t.projectId,projectRoot:r,revision:d,action:c,targetPath:n,summary:e.reason||`Patch ${n}`,diff:p,addedLines:h,removedLines:m});if(!S.approved)return this.auditLogger.log({session:t.id,project:t.projectId,tool:"project_patch",status:"rejected",metadata:{path:n,reason:S.reason}}),{success:!1,status:"REJECTED",filePath:n,addedLines:h,removedLines:m,diff:p,message:`STATUS: REJECTED - Patch approval denied: ${S.reason||"User rejected"}`};S.allowSessionWrites&&(v=!0)}try{if(this.sessionManager.assertAuthorized(),this.sessionManager.getWorkingDir(t)!==r)throw new Y("Session directory changed");if(this.permissionEngine.check(c,{allowAllWrites:t.allowAllWrites})==="deny")throw new Error("Permission revoked during approval");st(o),Sr(o,u)}catch(S){return{success:!1,status:S instanceof Y?"FILE_CHANGED":"ERROR",filePath:n,addedLines:h,removedLines:m,diff:p,message:S instanceof Error?S.message:"Safe write failed"}}v&&(t.allowAllWrites=!0);let w=oe.calculateHash(u);return this.sessionManager.recordChanged(t,n,w),this.auditLogger.log({session:t.id,project:t.projectId,tool:"project_patch",status:"success",metadata:{path:n,approved:!0,added:h,removed:m,newRevision:w}}),{success:!0,status:"APPLIED",filePath:n,addedLines:h,removedLines:m,diff:p,newRevision:w,message:`Patch successfully applied to ${n} (+${h} / -${m})`}}computePatchedContent(e,t){let r=t.trim();if(r.includes("<<<<<<< SEARCH")&&r.includes("=======")&&r.includes(">>>>>>> REPLACE"))return this.applySearchReplaceBlocks(e,r);let o=ye.applyPatch(e,t);if(typeof o=="string"&&o!=="")return o;if(t.includes("@@")){let n=`--- a/file
|
|
43
|
+
+++ b/file
|
|
44
|
+
${t}`,s=ye.applyPatch(e,n);if(typeof s=="string"&&s!=="")return s}throw new Error("Patch could not be applied cleanly. Ensure unified diff format or standard SEARCH/REPLACE blocks.")}applySearchReplaceBlocks(e,t){let r=e,o=/<<<<<<< SEARCH\r?\n([\s\S]*?)\r?\n=======\r?\n([\s\S]*?)\r?\n>>>>>>> REPLACE/g,n,s=!1;for(;(n=o.exec(t))!==null;){s=!0;let c=n[1],i=n[2];if(!r.includes(c))throw new Error(`Search block not found in target file:
|
|
45
|
+
${c.slice(0,100)}...`);r=r.replace(c,i)}if(!s)throw new Error("Invalid SEARCH/REPLACE format");return r}}});import{spawn as xo}from"node:child_process";import U from"node:fs";import J from"node:path";import ko from"node:crypto";function Ao(a){if(typeof a!="string"){if(!a.file||a.file.includes("\0")||a.args?.some(n=>n.includes("\0")))throw new Error("Invalid command arguments");return{file:a.file,args:[...a.args||[]]}}let e=[],t="",r="",o=!1;for(let n of a.trim()){if(/[`$\n\r\0]/.test(n))throw new Error("Shell syntax is unsupported; configure { file, args } instead");if(r){n===r?r="":t+=n;continue}if(n==='"'||n==="'")r=n,o=!0;else if(/\s/.test(n))o&&e.push(t),t="",o=!1;else{if(/[;&|<>]/.test(n)||n==="\\"&&process.platform!=="win32")throw new Error("Shell syntax is unsupported; configure { file, args } instead");t+=n,o=!0}}if(r)throw new Error("Unclosed command quote");if(o&&e.push(t),!e[0]||/^\w+=/.test(e[0]))throw new Error("Configure an executable and arguments without shell environment assignments");return{file:e[0],args:e.slice(1)}}var Fe,$t=j(()=>{"use strict";it();Ce();g(Ao,"parseCommand");Fe=class{constructor(e,t,r,o,n){this.projectManager=e;this.sessionManager=t;this.permissionEngine=r;this.approvalService=o;this.auditLogger=n}projectManager;sessionManager;permissionEngine;approvalService;auditLogger;static{g(this,"CommandRunner")}async runProjectCommand(e,t,r,o){if(o?.throwIfAborted(),!["typecheck","lint","test","format","build"].includes(t))throw new Error("Unsupported command category");let{id:n,root:s,config:c}=this.projectManager.getProject(e),i=this.sessionManager.getOrCreateSession(n,r),l=this.sessionManager.getWorkingDir(i,s),d=`command.${t}`,u=this.permissionEngine.check(d);if(u==="deny")throw new Error(`Permission denied: Executing '${t}' is set to DENY.`);let p=this.resolveCommand(l,t,c.commands);if(!p)throw new Error(`No command configured or detected for '${t}' in project '${n}'. Configure projects.${n}.commands.${t}`);if(p.file=this.resolveExecutable(p.file,l),process.platform==="win32"){if(/^npm(?:\.cmd)?$/i.test(J.basename(p.file))){let S=J.join(J.dirname(p.file),"node_modules/npm/bin/npm-cli.js");if(!U.existsSync(S))throw new Error("Cannot locate the installed npm CLI; configure Node and its CLI script");p.entryScript=U.realpathSync(S),p.args=[p.entryScript,...p.args],p.file=process.execPath}else if(/\.(?:cmd|bat)$/i.test(p.file))throw new Error("Windows batch launchers require an executable and explicit script arguments")}let f=JSON.stringify(p),h=this.snapshot(l,c,p);if(u==="ask"){let S=await ce(this.approvalService,this.sessionManager,{sessionId:i.id,projectId:n,projectRoot:l,revision:h,action:d,signal:o,summary:`Run trusted host code in ${l}: ${f}. This process can access host files and the network.`});if(!S.approved)throw new Error(`Execution of '${t}' was rejected: ${S.reason||"User rejected"}`)}o?.throwIfAborted(),this.sessionManager.assertAuthorized();let m=this.projectManager.getProject(n);if(m.root!==s||this.sessionManager.getWorkingDir(i)!==l||this.permissionEngine.check(d)!==u||this.snapshot(l,m.config,p)!==h)throw new Error("Command, project, executable, or package files changed during approval; retry");let v=Date.now(),w=t==="build"?6e5:["test","typecheck"].includes(t)?3e5:18e4;return new Promise(S=>{let x=!1,_=!1,ee=!1,te=!1,F={stdout:Buffer.alloc(0),stderr:Buffer.alloc(0)},H=xo(p.file,p.args,{cwd:l,env:G(),shell:!1,detached:process.platform!=="win32",stdio:["ignore","pipe","pipe"]}),ve=g(()=>{try{process.platform!=="win32"&&H.pid?process.kill(-H.pid,"SIGKILL"):H.kill("SIGKILL")}catch{}},"stop"),Se=g(()=>{ee=!0,ve()},"abort"),Fr=setTimeout(()=>{x=!0,ve()},w);o?.addEventListener("abort",Se,{once:!0}),o?.aborted&&Se();let dt=g((K,Pe)=>{let Oe=102400-F[K].length;F[K]=Buffer.concat([F[K],Pe.subarray(0,Math.max(0,Oe))]),Pe.length>Oe&&(_=!0,ve())},"collect");H.stdout.on("data",K=>dt("stdout",K)),H.stderr.on("data",K=>dt("stderr",K)),H.on("error",K=>{te=!0,dt("stderr",Buffer.from(K.message))}),H.on("close",K=>{clearTimeout(Fr),o?.removeEventListener("abort",Se),ve();let Pe=x||ee||_||te?1:K??1,Oe=Date.now()-v;this.auditLogger.log({session:i.id,project:n,tool:`project_${t}`,status:Pe===0?"success":"error",durationMs:Oe,metadata:{commandKey:t,exitCode:Pe,timedOut:x,canceled:ee,truncated:_}}),S({commandKey:t,command:`${J.basename(p.file)} [${t}]`,exitCode:Pe,stdout:F.stdout.toString("utf8"),stderr:F.stderr.toString("utf8"),durationMs:Oe,timedOut:x,truncated:_})})})}resolveCommand(e,t,r){if(r?.[t])return Ao(r[t]);let o={};try{o=JSON.parse(U.readFileSync(J.join(e,"package.json"),"utf8")).scripts||{}}catch{}if(typeof o[t]=="string")return{file:"npm",args:["run",t]};if(t==="typecheck"&&typeof o["type-check"]=="string")return{file:"npm",args:["run","type-check"]};if(t==="format"&&typeof o.prettier=="string")return{file:"npm",args:["run","prettier"]};let n=J.join(e,"node_modules","typescript","bin","tsc");return t==="typecheck"&&U.existsSync(J.join(e,"tsconfig.json"))&&U.existsSync(n)?{file:process.execPath,args:[n,"--noEmit"]}:null}resolveExecutable(e,t){let r=e.includes("/")||e.includes("\\")?[J.resolve(t,e)]:(process.env.PATH||"/usr/bin:/bin").split(J.delimiter).filter(Boolean).map(n=>J.resolve(n,e)),o=process.platform==="win32"?r.flatMap(n=>J.extname(n)?[n]:[n,...[".exe",".com",".cmd",".bat"].map(s=>n+s)]):r;for(let n of o)try{if(U.accessSync(n,U.constants.X_OK),U.statSync(n).isFile())return U.realpathSync(n)}catch{}throw new Error(`Executable '${e}' not found; configure an installed executable`)}snapshot(e,t,r){let o=ko.createHash("sha256");o.update(JSON.stringify({config:t,invocation:r,root:U.realpathSync(e),stat:this.identity(e),executable:this.identity(r.file),entryScript:r.entryScript?[this.identity(r.entryScript),U.readFileSync(r.entryScript,"utf8")]:void 0}));for(let n of["package.json","package-lock.json","npm-shrinkwrap.json","yarn.lock","pnpm-lock.yaml","tsconfig.json"]){let s=J.join(e,n);o.update(n),o.update(U.existsSync(s)?U.readFileSync(s):"absent")}return o.digest("hex")}identity(e){let t=U.statSync(e);return`${t.dev}:${t.ino}:${t.size}:${t.mtimeMs}:${t.ctimeMs}`}}});var Mt,We,Tt=j(()=>{"use strict";Mt=class extends Error{static{g(this,"PermissionDeniedError")}constructor(e,t){super(`Permission Denied for '${e}'${t?`: ${t}`:""}`),this.name="PermissionDeniedError"}},We=class{static{g(this,"PermissionEngine")}basePermissions;constructor(e){if(this.basePermissions=new Map([["project.read","allow"],["project.search","allow"],["project.write","ask"],["project.create","ask"],["project.delete","deny"],["command.typecheck","ask"],["command.lint","ask"],["command.test","ask"],["command.format","ask"],["command.build","ask"],["git.commit","ask"],["git.push","deny"]]),e)for(let[t,r]of Object.entries(e))r&&this.basePermissions.set(t,r)}check(e,t){return t?.allowAllWrites&&(e==="project.write"||e==="project.create")?"allow":t?.customPolicies?.has(e)?t.customPolicies.get(e):this.basePermissions.get(e)||"deny"}setPolicy(e,t){this.basePermissions.set(e,t)}getPolicy(e){return this.basePermissions.get(e)||"deny"}getAllPolicies(){let e={};for(let[t,r]of this.basePermissions.entries())e[t]=r;return e}}});import jr from"node:fs";import Ue from"node:path";function Eo(a){let{projectManager:e,sessionManager:t,safePatcher:r,commandRunner:o,auditLogger:n,permissionEngine:s,approvalService:c}=a;return{project_info:g(async({project:i})=>{try{let l=e.getProjectInfo(i);n.log({project:l.name,tool:"project_info",status:"success"});let{commands:d,...u}=l;return{text:JSON.stringify(u,null,2)}}catch(l){return{text:`Error: ${l.message}`,isError:!0}}},"project_info"),project_list:g(async(i,l)=>{try{let d=l?.allowedProjectId?{[l.allowedProjectId]:e.getProject(l.allowedProjectId).config}:e.listProjects();return{text:JSON.stringify(Object.fromEntries(Object.entries(d).map(([u,p])=>[u,{root:p.root,description:p.description}])),null,2)}}catch(d){return{text:`Error: ${d.message}`,isError:!0}}},"project_list"),project_tree:g(async({project:i,path:l,depth:d})=>{try{let u=e.getProject(i),p=t.getOrCreateSession(u.id),f=t.getWorkingDir(p,u.root),h=oe.getTree(f,l,{depth:d});return n.log({session:p.id,project:u.id,tool:"project_tree",status:"success",metadata:{path:l,depth:d,entryCount:h.entryCount}}),{text:h.tree}}catch(u){return{text:`Error: ${u.message}`,isError:!0}}},"project_tree"),project_read:g(async({project:i,path:l,startLine:d,endLine:u,sessionId:p})=>{try{let f=e.getProject(i),h=t.getOrCreateSession(f.id,p),m=t.getWorkingDir(h,f.root);if(s.check("project.read")==="deny")return{text:"Permission Denied: 'project.read' is set to DENY",isError:!0};let w=oe.readFile(m,l,d,u);return t.recordRead(h,w.relativePath,w.revision),n.log({session:h.id,project:f.id,tool:"project_read",status:"success",metadata:{path:w.relativePath,startLine:w.startLine,endLine:w.endLine,revision:w.revision}}),{text:[`File: ${w.relativePath}`,`Revision: ${w.revision}`,`Lines: ${w.startLine}-${w.endLine} of ${w.totalLines}`,`Language: ${w.language}`,`Modified: ${w.modifiedTime}`,"---",w.content].join(`
|
|
46
|
+
`)}}catch(f){return{text:`Error: ${f.message}`,isError:!0}}},"project_read"),project_search:g(async({project:i,query:l,path:d,maxResults:u,caseSensitive:p,sessionId:f})=>{try{let h=e.getProject(i),m=t.getOrCreateSession(h.id,f),v=t.getWorkingDir(m,h.root);if(s.check("project.search")==="deny")return{text:"Permission Denied: 'project.search' is set to DENY",isError:!0};let S=await Le.search(v,l,{path:d,maxResults:u,caseSensitive:p});return n.log({session:m.id,project:h.id,tool:"project_search",status:"success",metadata:{count:S.length}}),{text:JSON.stringify(S,null,2)}}catch(h){return{text:`Error: ${h.message}`,isError:!0}}},"project_search"),git_status:g(async({project:i,sessionId:l})=>{try{let d=e.getProject(i),u=t.getOrCreateSession(d.id,l),p=t.getWorkingDir(u,d.root),f=await Q.getStatus(p);return n.log({session:u.id,project:d.id,tool:"git_status",status:"success"}),{text:JSON.stringify(f,null,2)}}catch(d){return{text:`Error: ${d.message}`,isError:!0}}},"git_status"),git_diff:g(async({project:i,path:l,staged:d,base:u,sessionId:p})=>{try{let f=e.getProject(i),h=t.getOrCreateSession(f.id,p),m=t.getWorkingDir(h,f.root),v=await Q.getDiff(m,{path:l,staged:d,base:u});return n.log({session:h.id,project:f.id,tool:"git_diff",status:"success",metadata:{path:l,staged:d}}),{text:v||"(No diff)"}}catch(f){return{text:`Error: ${f.message}`,isError:!0}}},"git_diff"),session_start:g(async({project:i,useWorktree:l,baseBranch:d})=>{try{let u=e.getProject(i);if(l){let f=s.check("project.create");if(f==="deny")throw new Error("Permission denied: worktree creation");if(f==="ask"&&!(await ce(c,t,{action:"project.create",projectId:u.id,projectRoot:u.root,summary:`Create isolated worktree in ${u.root} from ${d||"HEAD"}`})).approved)throw new Error("Worktree creation rejected");if(e.getProject(u.id).root!==u.root||s.check("project.create")==="deny")throw new Error("Project or permission changed during approval")}let p=await t.createSession({projectId:u.id,useWorktree:l,baseBranch:d});return{text:JSON.stringify({sessionId:p.id,projectId:p.projectId,worktree:p.worktree||null,isolated:!!p.worktree,startedAt:p.startedAt.toISOString()},null,2)}}catch(u){return{text:`Error: ${u.message}`,isError:!0}}},"session_start"),session_list:g(async()=>{try{let i=t.listSessions().map(l=>({id:l.id,projectId:l.projectId,worktree:l.worktree||null,startedAt:l.startedAt.toISOString(),filesReadCount:l.filesRead.size,filesChangedCount:l.filesChanged.size,allowAllWrites:l.allowAllWrites}));return{text:JSON.stringify(i,null,2)}}catch(i){return{text:`Error: ${i.message}`,isError:!0}}},"session_list"),session_stop:g(async({sessionId:i,action:l,commitMessage:d})=>{try{let u=t.getSession(i);if(!u)throw new Error("Session not found");if(u.worktree&&l!=="keep"){let f=l==="commit"?"git.commit":"project.delete",h=s.check(f);if(h==="deny")throw new Error(`Permission denied: ${f}`);let m=t.getWorkingDir(u),v=await Q.getOperationSnapshot(m);if(h==="ask"&&!(await ce(c,t,{sessionId:i,action:f,projectId:u.projectId,projectRoot:m,revision:v,summary:`${l} session worktree ${i}${l==="commit"?` with message: ${d||"default"}`:" and delete its branch"}`})).approved)throw new Error("Session operation rejected");if(t.assertAuthorized(),s.check(f)!==h||t.getWorkingDir(u)!==m||await Q.getOperationSnapshot(m)!==v)throw new Error("Worktree or permission changed during approval; retry")}let p=await t.stopSession(i,l,d);return{text:JSON.stringify(p,null,2)}}catch(u){return{text:`Error: ${u.message}`,isError:!0}}},"session_stop"),project_patch:g(async({project:i,path:l,patch:d,reason:u,expectedRevision:p,sessionId:f})=>{try{let h=e.getProject(i),m=await r.applyPatch({projectId:h.id,path:l,patch:d,reason:u,expectedRevision:p,sessionId:f});return{text:JSON.stringify(m,null,2),isError:!m.success}}catch(h){return{text:`Error: ${h.message}`,isError:!0}}},"project_patch"),project_typecheck:g(async({project:i,sessionId:l},d)=>{try{let u=e.getProject(i),p=await o.runProjectCommand(u.id,"typecheck",l,d?.signal);return{text:JSON.stringify({command:p.command,exitCode:p.exitCode,stdout:p.stdout,stderr:p.stderr,durationMs:p.durationMs,timedOut:p.timedOut,truncated:p.truncated},null,2),isError:p.exitCode!==0}}catch(u){return{text:`Error: ${u.message}`,isError:!0}}},"project_typecheck"),project_lint:g(async({project:i,sessionId:l},d)=>{try{let u=e.getProject(i),p=await o.runProjectCommand(u.id,"lint",l,d?.signal);return{text:JSON.stringify({command:p.command,exitCode:p.exitCode,stdout:p.stdout,stderr:p.stderr,durationMs:p.durationMs,timedOut:p.timedOut,truncated:p.truncated},null,2),isError:p.exitCode!==0}}catch(u){return{text:`Error: ${u.message}`,isError:!0}}},"project_lint"),project_test:g(async({project:i,sessionId:l},d)=>{try{let u=e.getProject(i),p=await o.runProjectCommand(u.id,"test",l,d?.signal);return{text:JSON.stringify({command:p.command,exitCode:p.exitCode,stdout:p.stdout,stderr:p.stderr,durationMs:p.durationMs,timedOut:p.timedOut,truncated:p.truncated},null,2),isError:p.exitCode!==0}}catch(u){return{text:`Error: ${u.message}`,isError:!0}}},"project_test"),project_format:g(async({project:i,sessionId:l},d)=>{try{let u=e.getProject(i),p=await o.runProjectCommand(u.id,"format",l,d?.signal);return{text:JSON.stringify({command:p.command,exitCode:p.exitCode,stdout:p.stdout,stderr:p.stderr,durationMs:p.durationMs,timedOut:p.timedOut,truncated:p.truncated},null,2),isError:p.exitCode!==0}}catch(u){return{text:`Error: ${u.message}`,isError:!0}}},"project_format"),project_build:g(async({project:i,sessionId:l},d)=>{try{let u=e.getProject(i),p=await o.runProjectCommand(u.id,"build",l,d?.signal);return{text:JSON.stringify({command:p.command,exitCode:p.exitCode,stdout:p.stdout,stderr:p.stderr,durationMs:p.durationMs,timedOut:p.timedOut,truncated:p.truncated},null,2),isError:p.exitCode!==0}}catch(u){return{text:`Error: ${u.message}`,isError:!0}}},"project_build")}}var He,xr=j(()=>{"use strict";xt();tt();At();ot();Rt();Ot();$t();Tt();ge();it();g(Eo,"createHandlers");He=class{static{g(this,"LocalEngine")}services;handlers;protectedPaths;constructor(e){this.protectedPaths=e.protectedPaths||[];let t=new We(e.permissions),r=new Me(e.projects),o=new De(r,t,e.audit);this.services={projectManager:r,sessionManager:o,permissionEngine:t,approvalService:e.approval,auditLogger:e.audit,safePatcher:new Ne(e.approval,o,e.audit,t),commandRunner:new Fe(r,o,t,e.approval,e.audit)},this.handlers=Eo(this.services)}execute(e,t,r){return this.services.sessionManager.runWithContext(r||{},async()=>{try{this.services.sessionManager.assertAuthorized();let o=[...this.protectedPaths];for(let n of Object.values(this.services.projectManager.listProjects())){if(!jr.existsSync(n.root))continue;let s=jr.realpathSync(n.root);for(let c of n.protectedFiles||[])o.push(Ue.resolve(s,c));for(let c of this.services.sessionManager.listSessions())if(!(!c.worktree||c.projectRoot!==s))for(let i of[...this.protectedPaths,...(n.protectedFiles||[]).map(l=>Ue.resolve(s,l))]){let l=Ue.relative(s,i);!l.startsWith("..")&&!Ue.isAbsolute(l)&&o.push(Ue.join(c.worktree,l))}}return await I.withProtectedPaths(o,async()=>{let n=e==="project_search"?"project.search":["project_info","project_list","project_tree","project_read","git_status","git_diff","session_list"].includes(e)?"project.read":void 0;if(n){let c=this.services.permissionEngine.check(n);if(c==="deny")throw new Error(`Permission denied: ${n}`);if(c==="ask"&&!(await ce(this.services.approvalService,this.services.sessionManager,{action:n,summary:`Allow ${e}: ${JSON.stringify(t)}`})).approved)throw new Error("Read/search operation rejected");if(this.services.permissionEngine.check(n)!==c)throw new Error("Permission changed during approval")}let s=await this.handlers[e](t,r);return this.services.sessionManager.assertAuthorized(),this.services.auditLogger.log({tool:e,status:s.isError?"error":"success"}),s})}catch(o){return this.services.auditLogger.log({tool:e,status:"denied"}),{text:o instanceof Error?o.message:"Operation denied",isError:!0}}})}}});var kr=j(()=>{"use strict"});function at(a){if(a.noAuth)throw new Error("SECURITY ERROR: --no-auth / server.noAuth is not supported. HTTP MCP connections must authenticate with a Bearer token.");if(a.autoApprove)throw new Error("SECURITY ERROR: --auto-approve / server.autoApprove is not supported for HTTP. Sensitive operations requiring approval must be confirmed locally.")}var Ar=j(()=>{"use strict";g(at,"assertSafeHttpFlags")});var Lt,Er=j(()=>{"use strict";Lt=class{static{g(this,"ProjectChecks")}commandRunner;constructor(e){this.commandRunner=e}async runSuite(e,t,r){let o={},n=!0,s=[];for(let c of t)try{let i=await this.commandRunner.runProjectCommand(e,c,r);o[c]=i,i.exitCode!==0?(n=!1,s.push(`\u274C ${c} failed (exit code ${i.exitCode})`)):s.push(`\u2705 ${c} passed`)}catch(i){n=!1,s.push(`\u274C ${c} error: ${i.message}`)}return{passed:n,results:o,summary:s.join(`
|
|
47
|
+
`)}}}});var _r={};Bt(_r,{CommandRunner:()=>Fe,GitManager:()=>Q,LocalEngine:()=>He,PathGuard:()=>D,PermissionDeniedError:()=>Mt,PermissionEngine:()=>We,ProjectChecks:()=>Lt,ProjectFilesystem:()=>oe,ProjectManager:()=>Me,ProjectSearch:()=>Le,SafePatcher:()=>Ne,SecretGuard:()=>I,SecurityViolationError:()=>$,SessionManager:()=>De,WorktreeManager:()=>me,assertSafeHttpFlags:()=>at});var Dt=j(()=>{"use strict";xr();Rt();kr();Ar();Tt();ge();he();xt();At();Ot();tt();_t();ot();Er();$t()});import _o from"node:readline";var ct,Cr=j(()=>{"use strict";ct=class{constructor(e=!1){this.autoApprove=e}autoApprove;static{g(this,"TerminalApprovalService")}queue=Promise.resolve();setAutoApprove(e){this.autoApprove=e}requestApproval(e){let t=this.queue.then(()=>this.decide(e));return this.queue=t.catch(()=>{}),t}async decide(e){return e.signal?.aborted?{approved:!1,reason:"Operation canceled"}:e.expiresAt&&Date.now()>=e.expiresAt?{approved:!1,reason:"Approval expired"}:this.autoApprove?{approved:!0,reason:"Auto-approved by test service"}:process.stdin.isTTY?new Promise(t=>{let r=g(p=>process.stderr.write(p+`
|
|
48
|
+
`),"print"),o=g(p=>p.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g,"?"),"safe"),n=e.action==="project.write"||e.action==="project.create";e.projectId&&r(`Project: ${o(e.projectId)} (${o(e.projectRoot||"")})`),r(`
|
|
49
|
+
LOCAL APPROVAL: ${o(e.action)}
|
|
50
|
+
${o(e.targetPath||"Command / operation")}
|
|
51
|
+
${o(e.summary)}`),r(`[y] approve [n] reject [d] full diff${n?" [a] allow writes for this session":""}`);let s=_o.createInterface({input:process.stdin,output:process.stderr}),c=!1,i,l=g(p=>{c||(c=!0,i&&clearTimeout(i),e.signal?.removeEventListener("abort",d),s.close(),t(p))},"finish"),d=g(()=>l({approved:!1,reason:"Operation canceled"}),"abort");if(e.signal?.addEventListener("abort",d,{once:!0}),e.signal?.aborted){d();return}i=setTimeout(()=>l({approved:!1,reason:"Approval expired"}),Math.max(0,(e.expiresAt||Date.now()+3e5)-Date.now())),s.on("close",()=>l({approved:!1,reason:"Approval input closed"}));let u=g(()=>s.question(`Choice [y/n/d${n?"/a":""}]: `,p=>{let f=p.trim().toLowerCase();f==="y"||f==="a"&&n?l({approved:!0,allowSessionWrites:f==="a",reason:"Approved locally"}):f==="n"?l({approved:!1,reason:"Rejected locally"}):(r(f==="d"?o(e.diff||"No diff available"):"Invalid choice"),u())}),"ask");u()}):{approved:!1,reason:"Non-interactive environment; local approval required"}}}});import Ie from"node:fs";import Rr from"node:path";var lt,Ir=j(()=>{"use strict";lt=class a{static{g(this,"AuditLogger")}logFilePath;enabled;recentEntries=[];static MAX_RECENT=100;constructor(e=".agent/audit.log",t=!0){this.logFilePath=Rr.resolve(process.cwd(),e),this.enabled=t}log(e){if(!this.enabled)return;let t={timestamp:new Date().toISOString(),...e};this.recentEntries.push(t),this.recentEntries.length>a.MAX_RECENT&&this.recentEntries.shift();try{let r=Rr.dirname(this.logFilePath);if(Ie.existsSync(r)||Ie.mkdirSync(r,{recursive:!0,mode:448}),Ie.existsSync(this.logFilePath)&&Ie.lstatSync(this.logFilePath).isSymbolicLink())throw new Error("Audit path must not be a symlink");Ie.appendFileSync(this.logFilePath,JSON.stringify(t)+`
|
|
52
|
+
`,{encoding:"utf8",mode:384}),Ie.chmodSync(this.logFilePath,384)}catch{console.error("Failed to write to audit log; check storage permissions and path")}}getRecentLogs(e=20){return this.recentEntries.slice(-e)}getLogFilePath(){return this.logFilePath}}});function Or(a,e){for(let t of Object.keys(ft)){let r=ft[t];a.registerTool(t,{description:r.description,inputSchema:r.inputSchema},async(o,n)=>{let s=await e.execute(t,o,{signal:n.mcpReq.signal});return{content:[{type:"text",text:s.text}],...s.isError===void 0?{}:{isError:s.isError}}})}}var Nt,$r=j(()=>{"use strict";mt();Nt="2026-07-28";g(Or,"registerAgentTools")});import Co from"express";import Ft from"node:crypto";import{z as L}from"zod";function Lr(a){let{oauthManager:e,ownerToken:t,getProject:r}=a,o=Co.Router();o.use((i,l,d)=>{l.set({"Cache-Control":"no-store",Pragma:"no-cache","Referrer-Policy":"no-referrer","X-Content-Type-Options":"nosniff"}),d()});let n=g((i,l)=>{let d=e.getBaseUrl(i);l.json({issuer:d,authorization_endpoint:`${d}/oauth/authorize`,token_endpoint:`${d}/oauth/token`,registration_endpoint:`${d}/oauth/register`,revocation_endpoint:`${d}/oauth/revoke`,response_types_supported:["code"],grant_types_supported:["authorization_code","refresh_token"],code_challenge_methods_supported:["S256"],scopes_supported:["mcp"],token_endpoint_auth_methods_supported:["none","client_secret_basic","client_secret_post","private_key_jwt"],revocation_endpoint_auth_methods_supported:["none","client_secret_basic","client_secret_post","private_key_jwt"],token_endpoint_auth_signing_alg_values_supported:["RS256","PS256","ES256","EdDSA"],client_id_metadata_document_supported:!0,authorization_response_iss_parameter_supported:!0})},"handleAsm");o.get(["/.well-known/oauth-authorization-server","/.well-known/openid-configuration"],n),o.get(["/.well-known/oauth-protected-resource","/.well-known/oauth-protected-resource/mcp"],(i,l)=>{let d=e.getBaseUrl(i);l.json({resource:`${d}/mcp`,authorization_servers:[d],scopes_supported:["mcp"],bearer_methods_supported:["header"],resource_name:"Rynster Local Coding Agent"})}),o.post("/oauth/register",(i,l)=>{let d=Io.parse(i.body),u=d.token_endpoint_auth_method==="none"?void 0:`secret_${Ft.randomBytes(32).toString("base64url")}`,p=e.registerClient({clientId:`client_${Ft.randomBytes(24).toString("base64url")}`,clientName:d.client_name,redirectUris:d.redirect_uris,createdAt:Math.floor(Date.now()/1e3),issuer:e.getBaseUrl(i),authMethod:d.token_endpoint_auth_method,...u?{secretHash:T(u)}:{}});l.status(201).json({client_id:p.clientId,client_name:p.clientName,redirect_uris:p.redirectUris,client_id_issued_at:p.createdAt,token_endpoint_auth_method:p.authMethod,grant_types:["authorization_code","refresh_token"],response_types:["code"],...u?{client_secret:u,client_secret_expires_at:0}:{}})});function s(i,l){let d=new URL(l.redirectUri).origin;if(!/^[a-z]+:\/\/[a-z0-9._:[\]-]+$/i.test(d))throw new P("invalid_request","Invalid callback origin");i.set({"Referrer-Policy":"same-origin","Content-Security-Policy":`default-src 'none'; style-src 'unsafe-inline'; form-action 'self' ${d}; frame-ancestors 'none'; base-uri 'none'`,"X-Frame-Options":"DENY","Content-Type":"text/html; charset=utf-8"})}g(s,"consentHeaders"),o.get("/oauth/authorize",async(i,l)=>{let d=Ro.parse(i.query),u=e.getBaseUrl(i),p=r(),f=Tr(i)||Ft.randomBytes(32).toString("base64url"),h={clientId:d.client_id,redirectUri:d.redirect_uri,responseType:d.response_type,codeChallenge:d.code_challenge,codeChallengeMethod:d.code_challenge_method,issuer:u,resource:d.resource,scope:d.scope,state:d.state,projectId:p.id,projectKey:p.key},m=await e.beginConsent(h,f);l.cookie("rynster_consent",f,{httpOnly:!0,secure:u.startsWith("https:"),sameSite:"lax",path:"/oauth/authorize",maxAge:5*6e4}),s(l,m),l.send(e.renderConsentPage(m))}),o.post("/oauth/authorize",async(i,l)=>{let d=L.object({transaction:N.min(1),action:L.enum(["approve","deny"]),owner_token:N.optional()}).strict().parse(i.body),u=e.getBaseUrl(i);if(i.headers.origin&&i.headers.origin!==u)throw new P("invalid_request","Cross-origin consent rejected",403);let p=e.getConsent(d.transaction,Tr(i),u),f=r();if(f.id!==p.projectId||f.key!==p.projectKey)throw new P("invalid_request","Project changed; start consent again",403);if(d.action==="approve"&&!$e(d.owner_token?.trim()||"",t))return s(l,p),l.status(403).send(e.renderConsentPage(p,"Invalid owner authorization token. Check your terminal's Bearer Token and try again."));let h=await e.completeConsent(p,d.action==="approve"),m=new URL(p.redirectUri);m.searchParams.set("iss",u),p.state!==void 0&&m.searchParams.set("state",p.state),h?m.searchParams.set("code",h):m.searchParams.set("error","access_denied"),l.redirect(m.toString())});async function c(i,l){if(l.client_secret!==void 0&&l.client_assertion!==void 0)throw new P("invalid_client","Conflicting client authentication methods",401);let d=l.client_id,u=l.client_secret,p=u!==void 0?"client_secret_post":l.client_assertion?"private_key_jwt":"none";if(i.headers.authorization!==void 0){if(!/^Basic [A-Za-z0-9+/]+={0,2}$/i.test(i.headers.authorization)||l.client_secret!==void 0||l.client_assertion!==void 0)throw new P("invalid_client","Malformed or conflicting client authentication",401);let m=Buffer.from(i.headers.authorization.slice(6),"base64").toString("utf8"),v=m.indexOf(":");if(v<1)throw new P("invalid_client","Malformed Basic credentials",401);let w;try{w=decodeURIComponent(m.slice(0,v).replace(/\+/g," ")),u=decodeURIComponent(m.slice(v+1).replace(/\+/g," "))}catch{throw new P("invalid_client","Malformed Basic credentials",401)}if(d!==void 0&&d!==w)throw new P("invalid_client","Conflicting client identities",401);d=w,p="client_secret_basic"}let f=e.getBaseUrl(i),h=await e.resolveClient(we(d,"client_id"),f);if(p!==h.authMethod)throw new P("invalid_client","Registered client authentication method required",401);if(p.startsWith("client_secret_")&&(!u||!h.secretHash||!$e(T(u),h.secretHash)))throw new P("invalid_client","Invalid client secret",401);if(p==="private_key_jwt"){if(l.client_assertion_type!=="urn:ietf:params:oauth:client-assertion-type:jwt-bearer")throw new P("invalid_client","Invalid assertion type",401);await e.metadata.verifyAssertion(h.clientId,f,l.client_assertion,e.store,i.path==="/oauth/revoke"?"revoke":"token")}else if(l.client_assertion_type!==void 0)throw new P("invalid_client","Unexpected client assertion type",401);return h}return g(c,"authenticate"),o.post("/oauth/token",async(i,l)=>{let d=Mr.parse(i.body),u=await c(i,d),p=e.getBaseUrl(i),f=we(d.resource,"resource");if(f!==`${p}/mcp`)throw new P("invalid_target","Resource must match the canonical MCP URL");let h;if(d.grant_type==="authorization_code")h=e.exchangeCodeForToken({clientId:u.clientId,issuer:p,resource:f,code:we(d.code,"code"),redirectUri:we(d.redirect_uri,"redirect_uri"),codeVerifier:we(d.code_verifier,"code_verifier")});else if(d.grant_type==="refresh_token")h=e.refreshAccessToken({clientId:u.clientId,issuer:p,resource:f,refreshToken:we(d.refresh_token,"refresh_token")});else throw new P("unsupported_grant_type","Supported grants: authorization_code, refresh_token");l.json({access_token:h.accessToken,refresh_token:h.refreshToken,token_type:"Bearer",expires_in:h.expiresIn,scope:"mcp"})}),o.post("/oauth/revoke",async(i,l)=>{let d=Mr.parse(i.body),u=await c(i,d);e.revoke(we(d.token,"token"),u.clientId,e.getBaseUrl(i)),l.status(200).end()}),o.use((i,l,d,u)=>{i instanceof B?(console.error(i.message),d.status(503).json({error:"temporarily_unavailable",error_description:"OAuth storage unavailable; inspect the Rynster terminal"})):i instanceof P?(i.status===401&&d.setHeader("WWW-Authenticate",'Basic realm="rynster"'),d.status(i.status).json({error:i.error,error_description:i.message})):i instanceof L.ZodError?d.status(400).json({error:"invalid_request",error_description:"Malformed or unsupported OAuth parameters"}):d.status(400).json({error:"invalid_request",error_description:"Invalid OAuth request"})}),o}var N,Ro,Io,Mr,we,Tr,Dr=j(()=>{"use strict";Xe();ke();Pt();N=L.string().max(4096),Ro=L.object({client_id:N.min(1),redirect_uri:N.min(1),response_type:L.literal("code"),code_challenge:N.min(1),code_challenge_method:L.literal("S256"),resource:N.min(1),scope:L.literal("mcp").default("mcp"),state:N.optional()}),Io=L.object({client_name:L.string().min(1).max(200).default("AI Client"),redirect_uris:L.array(St).min(1).max(20),token_endpoint_auth_method:L.enum(["none","client_secret_basic","client_secret_post"]).default("client_secret_basic"),grant_types:L.array(L.enum(["authorization_code","refresh_token"])).refine(a=>a.includes("authorization_code")).optional(),response_types:L.array(L.literal("code")).min(1).optional()}),Mr=L.object({grant_type:N.optional(),client_id:N.optional(),client_secret:N.optional(),code:N.optional(),redirect_uri:N.optional(),code_verifier:N.optional(),resource:N.optional(),refresh_token:N.optional(),scope:L.literal("mcp").optional(),client_assertion:L.string().max(16384).optional(),client_assertion_type:N.optional(),token:N.optional()}),we=g((a,e)=>{if(!a)throw new P("invalid_request",`Missing ${e}`);return a},"requireValue"),Tr=g(a=>{let e=(a.headers.cookie||"").split(";").map(r=>r.trim()).filter(r=>r.startsWith("rynster_consent="));if(e.length!==1)return"";let t=e[0].slice(16);return/^[A-Za-z0-9_-]{43}$/.test(t)?t:""},"browserCookie");g(Lr,"createOAuthRouter")});var Ut={};Bt(Ut,{AgentServer:()=>Wt});import Oo from"express";import Nr from"node:fs";import{AsyncLocalStorage as $o}from"node:async_hooks";import{McpServer as Mo,createMcpHandler as To}from"@modelcontextprotocol/server";import{serveStdio as Lo}from"@modelcontextprotocol/server/stdio";import{toNodeHandler as Do}from"@modelcontextprotocol/node";import{createMcpExpressApp as No}from"@modelcontextprotocol/express";var Wt,Ht=j(()=>{"use strict";yt();jt();Dt();Cr();Ir();$r();Xe();Dr();Wt=class{static{g(this,"AgentServer")}configManager;permissionEngine;projectManager;sessionManager;approvalService;auditLogger;safePatcher;commandRunner;oauthManager;autoApproveEnabled;localEngine;stdioHandle;httpHandlers=new Set;httpServers=new Set;activeHttpPolicy;requestAuth=new $o;constructor(e,t,r){this.configManager=e||new q;let o=this.configManager.getConfig();this.oauthManager=new Ye(t,void 0,r),this.autoApproveEnabled=o.server.autoApprove??!1,this.auditLogger=new lt(o.audit.logFile,o.audit.enabled),this.approvalService=new ct(this.autoApproveEnabled),this.localEngine=new He({projects:this.configManager,permissions:o.permissions,audit:this.auditLogger,approval:this.approvalService,protectedPaths:[this.configManager.getConfigPath(),this.auditLogger.getLogFilePath(),this.oauthManager.store.file]});let n=this.localEngine.services;this.permissionEngine=n.permissionEngine,this.projectManager=n.projectManager,this.sessionManager=n.sessionManager,this.safePatcher=n.safePatcher,this.commandRunner=n.commandRunner}getContext(){return{projectManager:this.projectManager,sessionManager:this.sessionManager,safePatcher:this.safePatcher,commandRunner:this.commandRunner,auditLogger:this.auditLogger,permissionEngine:this.permissionEngine,approvalService:this.approvalService}}createMcpServerInstance(){let e=new Mo({name:Ee,version:_e},{supportedProtocolVersions:[Nt]});return Or(e,{execute:g(async(r,o,n)=>{let s=this.requestAuth.getStore();try{if(!s)return this.localEngine.execute(r,o,{...n,ownerId:"stdio"});if(s.owner)return this.localEngine.execute(r,o,{...n,ownerId:"rynster-owner"});let c=this.oauthManager.getTokenInfo(s.token,s.issuer);if(!c)throw new Error("Authorization grant expired or revoked");let i=g(()=>{let d=this.oauthManager.getTokenInfo(s.token,s.issuer);if(!d||d.id!==c.id)throw new Error("Authorization grant expired or revoked");let u=this.projectManager.getProject(c.projectId);if(Ze(u.id,u.root)!==c.projectKey)throw new Error("Consented project changed; reconnect")},"assertAuthorized");i();let l=o;if(l.project&&l.project!==c.projectId)throw new Error("Project is outside the consented grant");return this.localEngine.execute(r,{...o,project:c.projectId},{...n,ownerId:c.id,allowedProjectId:c.projectId,assertAuthorized:i})}catch(c){return{text:c instanceof Error?c.message:"Authorization denied",isError:!0}}},"execute")}),e}setAutoApprove(e){if(e&&this.stdioHandle)throw new Error("SECURITY ERROR: automatic approval is unsupported in stdio");e&&this.activeHttpPolicy&&at({...this.activeHttpPolicy,autoApprove:!0}),this.autoApproveEnabled=e,this.approvalService.setAutoApprove(e)}async startStdio(){if(this.autoApproveEnabled)throw new Error("SECURITY ERROR: server.autoApprove is no longer supported. Sensitive operations must be approved locally.");this.stdioHandle=Lo(()=>this.createMcpServerInstance(),{legacy:"reject",onerror:g(()=>console.error("MCP stdio request failed"),"onerror")}),console.error("rynster running on modern MCP stdio")}async startHttp(e={}){let t=this.configManager.getConfig(),r=e.port??t.server.port??3333;if(!Number.isInteger(r)||r<0||r>65535)throw new Error("Port must be an integer from 0 to 65535");let o=e.host||t.server.host||"127.0.0.1",n=e.noAuth??t.server.noAuth??!1,s=e.autoApprove??this.autoApproveEnabled;at({host:o,noAuth:n,autoApprove:s}),this.setAutoApprove(s);let c=e.authToken||this.configManager.getOrCreateAuthToken(),i=pr(e.publicUrl,process.env.RYNSTER_PUBLIC_URL,t.server.publicUrl);this.oauthManager.setPublicUrl(i);let l=["localhost","127.0.0.1","[::1]",o];i&&l.push(new URL(i).hostname);let d=new Set(i?[i]:[]),u=No({host:o,allowedHosts:l,allowedOrigins:l});u.use((h,m,v)=>{let w=h.headers.origin,S=`http://${h.headers.host}`,x=new URL(S).hostname,_=["localhost","127.0.0.1","[::1]"].includes(x)&&w===S;if(w&&(w==="null"||!d.has(w)&&!_)){m.status(403).json({error:"untrusted_origin"});return}if(w&&(m.setHeader("Access-Control-Allow-Origin",w),m.setHeader("Vary","Origin"),m.setHeader("Access-Control-Allow-Headers","Authorization, Content-Type, MCP-Protocol-Version, MCP-Session-Id"),m.setHeader("Access-Control-Allow-Methods","GET, POST, DELETE, OPTIONS")),h.method==="OPTIONS"){m.sendStatus(204);return}v()}),u.use(Oo.urlencoded({extended:!0})),u.use((h,m,v)=>{console.log(`[HTTP] ${h.method} ${["/","/mcp","/health","/oauth/token","/oauth/authorize","/oauth/register","/oauth/revoke"].includes(h.path)?h.path:"other"}`),v()}),u.all(["/sse","/messages"],(h,m)=>{m.status(410).json({error:"transport_removed",message:"Use modern MCP at /mcp (protocol 2026-07-28), or modern stdio."})}),u.post("/",(h,m)=>{m.setHeader("Allow","GET"),m.status(405).json({error:"endpoint_removed",message:"MCP requests must use /mcp."})}),u.use((h,m,v)=>{if(h.path==="/"||h.path==="/health"||h.path==="/.well-known/oauth-authorization-server"||h.path==="/.well-known/openid-configuration"||h.path==="/.well-known/oauth-protected-resource"||h.path==="/.well-known/oauth-protected-resource/mcp"||h.path.startsWith("/oauth/")||n)return v();let x=h.headers.authorization?.match(/^Bearer ([^\s]+)$/i)?.[1],_=this.oauthManager.getBaseUrl(h),ee=`${_}/.well-known/oauth-protected-resource/mcp`;if(!x)return m.setHeader("WWW-Authenticate",`Bearer error="invalid_token", scope="mcp", resource_metadata="${ee}"`),m.status(401).json({jsonrpc:"2.0",error:{code:-32e3,message:"Unauthorized: Missing authentication token. Pass Bearer token or complete OAuth sign-in."},id:null});let te=$e(x,c),F=te?void 0:this.oauthManager.getTokenInfo(x,_),H=te||!!F;if(F)try{let Se=this.projectManager.getProject(F.projectId);H=Ze(Se.id,Nr.realpathSync(Se.root))===F.projectKey}catch{H=!1}if(!H)return m.setHeader("WWW-Authenticate",`Bearer error="invalid_token", error_description="Invalid or expired token", resource_metadata="${ee}"`),m.status(401).json({jsonrpc:"2.0",error:{code:-32e3,message:"Unauthorized: Invalid or expired authentication token"},id:null});let ve={token:x,clientId:te?"rynster-owner":F.clientId,scopes:["mcp"],...F?{expiresAt:Math.floor(F.accessExpiresAt/1e3)}:{}};h.auth=ve,this.requestAuth.run({token:x,issuer:_,owner:te},v)}),u.use(Lr({oauthManager:this.oauthManager,ownerToken:c,getProject:g(()=>{let h=this.projectManager.getProject();return{id:h.id,key:Ze(h.id,Nr.realpathSync(h.root))}},"getProject")})),u.get("/health",(h,m)=>{let v=this.projectManager.listProjects();m.json({status:"ok",server:Ee,version:_e,registeredProjects:Object.keys(v),timestamp:new Date().toISOString()})}),u.get("/",(h,m)=>{let v=this.oauthManager.getBaseUrl(h);m.json({status:"ok",server:Ee,version:_e,protocol_versions:[Nt],mcp_endpoint:`${v}/mcp`,health:`${v}/health`,registeredProjects:Object.keys(this.projectManager.listProjects())})});let p=To(()=>this.createMcpServerInstance(),{legacy:"reject",onerror:g(()=>console.error("MCP HTTP request failed"),"onerror")});this.httpHandlers.add(p);let f=Do(p,{onerror:g(()=>console.error("MCP HTTP adapter request failed"),"onerror")});return u.all("/mcp",async(h,m)=>{await f(h,m,h.body)}),u.use((h,m,v,w)=>{h instanceof B&&console.error(h.message);let S=h?.type,x=S==="entity.parse.failed"?400:S==="entity.too.large"?413:h instanceof B?503:500;v.status(x).json({error:x<500?"invalid_request":"temporarily_unavailable"})}),new Promise((h,m)=>{let v=u.listen(r,o,()=>{let w=v.address(),S=typeof w=="object"&&w?w.port:r,x=["0.0.0.0","::"].includes(o)?"127.0.0.1":o,_=`http://${x.includes(":")?`[${x}]`:x}:${S}`;if(!i&&!["localhost","127.0.0.1","::1","[::1]"].includes(x)){v.close(),m(new Error("Configure --public-url for a non-loopback listener"));return}this.oauthManager.setLocalUrl(_),this.activeHttpPolicy={host:o,noAuth:n},v.on("close",()=>{this.activeHttpPolicy=void 0,this.httpServers.delete(v),this.httpHandlers.delete(p),p.close()}),h({httpServer:v,port:S,host:o,token:c,publicUrl:i,connectorUrl:`${i||_}/mcp`})});this.httpServers.add(v),v.once("error",w=>{this.httpServers.delete(v),this.httpHandlers.delete(p),p.close(),m(w)})})}async close(){await Promise.all([...this.httpHandlers].map(e=>e.close())),await this.stdioHandle?.close(),this.stdioHandle=void 0,await Promise.all([...this.httpServers].map(e=>new Promise((t,r)=>{e.close(o=>o?r(o):t())})))}getConfigManager(){return this.configManager}getSessionManager(){return this.sessionManager}getProjectManager(){return this.projectManager}}});yt();import{Command as Fo}from"commander";import pt from"node:path";import Wo from"node:fs";Xe();async function dr(a,e=fetch){let t=new URL(a);if(t.pathname!=="/mcp"||t.search||t.hash||t.username||t.password)throw new Error("Doctor URL must be a canonical HTTPS /mcp URL without credentials");let r=ue(t.origin),o=[];async function n(c){let i=await e(`${r}${c}`,{redirect:"error",signal:AbortSignal.timeout(5e3)});if(!i.ok)throw new Error(`Discovery returned HTTP ${i.status}`);if(!i.body)throw new Error("Missing discovery body");let l=i.body.getReader(),d=[],u=0;try{for(;;){let{done:f,value:h}=await l.read();if(f)break;if(u+=h.length,u>64*1024)throw new Error("Discovery exceeds 64 KiB");d.push(h)}}finally{await l.cancel()}let p=JSON.parse(Buffer.concat(d).toString("utf8"));if(!p||typeof p!="object"||Array.isArray(p))throw new Error("Malformed discovery metadata");return p}g(n,"json");async function s(c,i){try{await i(),o.push({check:c,ok:!0,detail:"passed"})}catch(l){o.push({check:c,ok:!1,detail:l instanceof Error?l.message:"failed"})}}return g(s,"check"),await s("Authentication challenge",async()=>{let c=await e(t,{redirect:"error",signal:AbortSignal.timeout(5e3)});await c.body?.cancel();let i=c.headers.get("www-authenticate")||"";if(c.status!==401||!i.startsWith("Bearer ")||!i.includes(`resource_metadata="${r}/.well-known/oauth-protected-resource/mcp"`))throw new Error("Expected 401 with canonical resource_metadata challenge; set Rynster's --public-url")}),await s("Protected resource discovery",async()=>{let c=await n("/.well-known/oauth-protected-resource/mcp");if(c.resource!==`${r}/mcp`||!Array.isArray(c.authorization_servers)||c.authorization_servers.length!==1||c.authorization_servers[0]!==r)throw new Error("Resource or authorization server differs from the connector origin")}),await s("Authorization server discovery",async()=>{let c=await n("/.well-known/oauth-authorization-server");for(let[i,l]of Object.entries({issuer:r,authorization_endpoint:`${r}/oauth/authorize`,token_endpoint:`${r}/oauth/token`,registration_endpoint:`${r}/oauth/register`,revocation_endpoint:`${r}/oauth/revoke`}))if(c[i]!==l)throw new Error(`${i} differs from the canonical origin`);if(!c.code_challenge_methods_supported?.includes("S256")||!c.client_id_metadata_document_supported||!c.authorization_response_iss_parameter_supported)throw new Error("S256, CIMD or issuer response support is missing")}),o}g(dr,"diagnoseConnection");ke();Je();Je();function io(a){let e=new pe(a).readSnapshot(),t=Date.now();return e.grants.map(r=>{let o=e.clients.find(c=>c.clientId===r.clientId&&c.issuer===r.issuer),n=o?.clientName||r.clientId;if(!o&&r.clientId.startsWith("https://"))try{n=new URL(r.clientId).hostname}catch{}let s=e.access.filter(c=>c.grantId===r.id);return{id:r.id,clientId:r.clientId,clientName:n,projectId:r.projectId,issuer:r.issuer,status:r.revoked?"revoked":r.expiresAt<=t?"expired":"active",expiresAt:new Date(r.expiresAt).toISOString(),accessExpiresAt:s.length?new Date(Math.max(...s.map(c=>c.expiresAt))).toISOString():null,refreshCount:e.refresh.filter(c=>c.grantId===r.id&&c.used).length}})}g(io,"listLocalOAuthGrants");function ao(a,e){let t=new pe(a);if(!t.readSnapshot().grants.some(r=>r.id===e))throw new Error("Grant not found; use a complete ID from 'rynster oauth grants'. Nothing revoked.");return t.transact(r=>{let o=r.grants.find(s=>s.id===e);if(!o)throw new P("invalid_grant","Grant no longer exists; nothing revoked");let n=o.revoked;return o.revoked=!0,{alreadyRevoked:n}})}g(ao,"revokeLocalOAuthGrant");var ie=g(a=>a.replace(/[\p{Cc}\p{Cf}]/gu,"?"),"safe");function ur(a){process.exitCode=1,console.error(ie(a instanceof Error?a.message:"OAuth grant command failed"))}g(ur,"reportError");function gr(a){let e=a.command("oauth").description("Inspect and revoke local OAuth grants");e.command("grants").description("List grant metadata without displaying credentials or token hashes").option("--storage <path>","OAuth store path (overrides RYNSTER_OAUTH_STORAGE_PATH)").option("--json","Print credential-free metadata as JSON").action(t=>{try{let r=xe(t.storage),o=io(r);if(t.json){console.log(JSON.stringify({storagePath:r,grants:o},null,2));return}console.log(`OAuth store: ${ie(r)}`),o.length||console.log("No OAuth grants found.");for(let n of o)console.log(`
|
|
53
|
+
Grant: ${ie(n.id)}`),console.log(` Client: ${ie(n.clientName)} (${ie(n.clientId)})`),console.log(` Project: ${ie(n.projectId)}`),console.log(` Issuer: ${ie(n.issuer)}`),console.log(` Status: ${n.status}`),console.log(` Grant expires: ${n.expiresAt}`),console.log(` Latest access expires: ${n.accessExpiresAt||"none recorded"}`),console.log(` Successful refreshes: ${n.refreshCount}`)}catch(r){ur(r)}}),e.command("revoke <grant-id>").description("Revoke exactly one local grant and its access/refresh authorization").option("--storage <path>","OAuth store path (overrides RYNSTER_OAUTH_STORAGE_PATH)").option("--json","Print the revocation result as JSON").action((t,r)=>{try{let o=xe(r.storage),n=ao(o,t);if(r.json){console.log(JSON.stringify({storagePath:o,id:t,status:"revoked",...n},null,2));return}console.log(`${n.alreadyRevoked?"Already revoked":"Revoked"} grant ${ie(t)}.`),console.log("This grant cannot authorize tool calls or token renewal. Reconnect for fresh consent.")}catch(o){ur(o)}})}g(gr,"registerOAuthCommands");jt();var V=new Fo;V.name(Ee).description("Provider-neutral local coding agent with MCP, file guards, and local approvals").version(_e).addHelpText("after",`
|
|
54
|
+
Support Rynster: ${bt}`);V.command("start [project]").description("Starts the MCP agent server for a registered project or path").option("-p, --port <number>","HTTP port for remote MCP (defaults to config or 3333)").option("-h, --host <host>","Host interface to bind (defaults to config or 127.0.0.1)").option("-s, --stdio","Run in stdio mode (for clients supporting MCP 2026-07-28)").option("-t, --token <token>","Custom bearer token").option("-u, --public-url <url>","Public HTTPS tunnel origin (e.g. https://my-tunnel.ngrok-free.app)").option("-c, --config <path>","Custom configuration file path").action(async(a,e)=>{let t=new q(e.config);if(a)if(Wo.existsSync(pt.resolve(a))){let n=pt.resolve(a),s=pt.basename(n);t.registerProject(s,{root:n}),t.getConfig().defaultProject=s}else t.getProject(a)||(console.error(`Error: Project '${a}' is not registered.`),console.error(`Available projects: ${Object.keys(t.listProjects()).join(", ")||"(none)"}`),process.exit(1)),t.getConfig().defaultProject=a;let{AgentServer:r}=await Promise.resolve().then(()=>(Ht(),Ut)),o=new r(t);if(o.getProjectManager().getProject(),e.stdio)await o.startStdio();else{let n=e.port===void 0?void 0:Number(e.port);if(n!==void 0&&(!Number.isInteger(n)||n<0||n>65535))throw new Error("Port must be an integer from 0 to 65535");let s=e.host,{port:c,host:i,publicUrl:l,connectorUrl:d}=await o.startHttp({port:n,host:s,authToken:e.token,publicUrl:e.publicUrl}),u=o.getProjectManager().getProjectInfo();console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),console.log("\u2502 RYNSTER \u2502"),console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"),console.log(` Project: ${u.name} (${u.framework}, branch: ${u.branch})`),console.log(` Project Root: ${u.root}`);let p=i.includes(":")?`[${i}]`:i;console.log(` MCP Endpoint: http://${p}:${c}/mcp`),console.log(` Health Check: http://${p}:${c}/health`),console.log(" Owner Token: Stored privately. Run rynster token to display it explicitly."),console.log(` Connector URL: ${d}`),l?(console.log(` Public Origin: ${l}`),console.log(` OAuth Metadata: ${l}/.well-known/oauth-authorization-server`)):console.log(" Remote OAuth: Set --public-url to your HTTPS tunnel origin, then restart."),console.log(""),console.log(" Permissions:"),console.log(" Read: ALLOW"),console.log(" Search: ALLOW"),console.log(" Write/Create: ASK (approval required for 'ask' policy)"),console.log(" Delete: DENY"),console.log(` Tests: ${t.getPermission("command.test").toUpperCase()}`),console.log(` Lint: ${t.getPermission("command.lint").toUpperCase()}`),console.log(" Git Commit: ASK"),console.log(" Git Push: DENY"),console.log(""),console.log(" Ready for MCP clients supporting protocol 2026-07-28."),console.log(" Press Ctrl+C to stop.");let f=g(async()=>{console.log(`
|
|
55
|
+
Shutting down Rynster...`),await o.close()},"shutdown");process.once("SIGINT",f),process.once("SIGTERM",f)}});V.command("register <id> <projectPath>").description("Registers a project directory with the agent").option("-c, --config <path>","Custom configuration file path").action((a,e,t)=>{let r=new q(t.config),o=pt.resolve(e);r.registerProject(a,{root:o}),console.log(`Successfully registered project '${a}' -> ${o}`)});V.command("unregister <id>").description("Unregisters a project").option("-c, --config <path>","Custom configuration file path").action((a,e)=>{let r=new q(e.config).unregisterProject(a);console.log(r?`Project '${a}' unregistered.`:`Project '${a}' was not found.`)});V.command("projects").description("Lists all registered projects").option("-c, --config <path>","Custom configuration file path").action(a=>{let e=new q(a.config),t=e.listProjects(),r=e.getConfig().defaultProject;console.log(`
|
|
56
|
+
Registered Projects:`),console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");let o=Object.entries(t);if(o.length===0)console.log(" (None registered. Use 'rynster register <id> <path>')");else for(let[n,s]of o)console.log(` \u2022 ${n}${n===r?" (default)":""}: ${s.root}`);console.log("")});V.command("status [project]").description("Shows status and git state of a project").option("-c, --config <path>","Custom configuration file path").action(async(a,e)=>{let{AgentServer:t}=await Promise.resolve().then(()=>(Ht(),Ut)),{GitManager:r}=await Promise.resolve().then(()=>(Dt(),_r)),o=new q(e.config),n=new t(o);try{let s=n.getProjectManager().getProjectInfo(a);if(console.log(`
|
|
57
|
+
Project Status:`),console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),console.log(` Name: ${s.name}`),console.log(` Root: ${s.root}`),console.log(` Framework: ${s.framework}`),console.log(` Package Mgr: ${s.packageManager}`),console.log(` Git Branch: ${s.branch}`),s.hasGit){let i=await r.getStatus(s.root);console.log(` Git Clean: ${i.isClean}`),console.log(` Modified Files: ${i.modified.length}`),console.log(` Untracked: ${i.untracked.length}`)}let c=n.getSessionManager().listSessions();console.log(` Active Sessions: ${c.length}`),console.log("")}catch(s){process.exitCode=1,console.error(`Error: ${s.message}`)}});V.command("doctor").description("Checks MCP OAuth discovery without registering clients or sending credentials").requiredOption("--url <url>","Canonical HTTPS MCP connector URL ending in /mcp").action(async a=>{try{let e=await dr(a.url);for(let t of e)console.log(`${t.ok?"PASS":"FAIL"} ${t.check}: ${t.detail}`);console.log("Real client onboarding, tool access, and token renewal require separate validation."),e.some(t=>!t.ok)&&(process.exitCode=1)}catch(e){console.error(e instanceof Error?e.message:"Connection diagnostics failed"),process.exitCode=1}});V.command("token").description("Explicitly displays the private owner token for local OAuth consent").option("-c, --config <path>","Custom configuration file path").action(a=>console.log(new q(a.config).getOrCreateAuthToken()));V.command("support").description("Show the optional Ko-fi support link for Rynster").action(()=>console.log(`Support Rynster: ${bt}`));gr(V);await V.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rynster",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Provider-neutral local coding agent MCP server",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"email": "baskar12ai@gmail.com"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
10
|
+
"funding": {
|
|
11
|
+
"type": "ko-fi",
|
|
12
|
+
"url": "https://ko-fi.com/bas12"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"ai",
|
|
17
|
+
"coding-agent",
|
|
18
|
+
"claude",
|
|
19
|
+
"chatgpt",
|
|
20
|
+
"local"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"prepack": "node ../../scripts/release.mjs prepack",
|
|
24
|
+
"prepublishOnly": "node ../../scripts/release.mjs publish-check"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.17 <23 || >=24.2 <25"
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"bin": {
|
|
35
|
+
"rynster": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/index.js",
|
|
39
|
+
"README.md",
|
|
40
|
+
"CHANGELOG.md",
|
|
41
|
+
"agent.config.example.json",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"SECURITY.md"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/express": "^2.0.2",
|
|
47
|
+
"@modelcontextprotocol/node": "^2.1.1",
|
|
48
|
+
"@modelcontextprotocol/server": "^2.3.1",
|
|
49
|
+
"@vscode/ripgrep": "^1.18.0",
|
|
50
|
+
"commander": "^15.0.0",
|
|
51
|
+
"cors": "^2.8.6",
|
|
52
|
+
"diff": "^9.0.0",
|
|
53
|
+
"express": "^5.2.1",
|
|
54
|
+
"jose": "^6.2.12",
|
|
55
|
+
"zod": "^4.6.5"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@rynster/contracts": "*",
|
|
59
|
+
"@rynster/local-engine": "*",
|
|
60
|
+
"@rynster/mcp-adapter": "*"
|
|
61
|
+
}
|
|
62
|
+
}
|