rynster 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Rynster release notes
2
+
3
+ ## 0.1.1 — 2026-10-24
4
+
5
+ Documentation and packaging update. No change to supported runtimes or the
6
+ permission model.
7
+
8
+ - Update the README's release-file install example to the current version.
9
+
10
+ ## 0.1.0 — 2026-10-18
11
+
12
+ Rynster connects MCP clients to local coding projects through stdio or an
13
+ authenticated HTTP endpoint. This first npm release uses the `rynster` package
14
+ name and executable.
15
+
16
+ - Register local projects and expose their file, search, Git, and workflow tools.
17
+ - Approve edits and configured development commands from the local terminal.
18
+ - Protect credentials and project boundaries; optionally work in Git worktrees.
19
+ - Connect remote clients through a user-managed HTTPS tunnel with project-scoped
20
+ OAuth grants, automatic refresh, and local grant listing/revocation commands.
21
+ - Diagnose HTTP OAuth discovery with `rynster doctor`.
22
+ - Display the optional Ko-fi link with `rynster support`.
23
+ - Protect Windows credentials with native ACLs, reject linked credential stores and alternate data streams, and support installed npm commands without a batch launcher.
24
+ - Apply Windows ACLs without loading PowerShell modules; retain strict file identities by requiring runtimes with the upstream libuv fix.
25
+ - Validate source line endings and exercise the release workflow on both Linux and Windows CI runners.
26
+
27
+ Supported runtimes are Node.js 22.17+ within 22.x and Node.js 24.2+ within 24.x. Linux and Windows
28
+ have separate verification records; macOS has not been verified. Configured project
29
+ scripts execute with the host user's permissions and are not OS-sandboxed. See
30
+ the packaged `SECURITY.md` for the trust boundary and remaining race limitations.
31
+
32
+ This release starts fresh with `RYNSTER_*` settings and `~/.rynster/oauth.json`.
33
+ Earlier development product settings and credential stores are not imported.
34
+ Claude/ChatGPT onboarding and reads were exercised with earlier development builds
35
+ under the old package name; those results do not verify live onboarding with this
36
+ renamed tarball. Additional ChatGPT token-renewal and other lifecycle checks are
37
+ deferred.
38
+
39
+ The CLI is proprietary and free to use for personal and internal business
40
+ purposes, including commercial work and client projects. Redistribution, resale,
41
+ modification, and offering the CLI itself as a service require separate written
42
+ permission. See the packaged LICENSE for complete terms. Third-party components
43
+ retain their own licenses. Rynster Connect remains proprietary under separate
44
+ terms and is not included in this release. The source repository remains private.
45
+ Copyright holder: Baskar Ellangudi. Report security vulnerabilities privately to
46
+ <baskar12ai@gmail.com>; see SECURITY.md for reporting instructions.
47
+
48
+ 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,238 @@
1
- # Temporary Holding Version
1
+ # Rynster
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Let your AI assistant work on your local code projects, with you in control.**
4
+
5
+ Rynster is a Provider-neutral local tool that connects AI clients (such as Claude or ChatGPT) to projects on your computer through [MCP](https://modelcontextprotocol.io). The assistant can read code, search files, review Git changes, and propose edits. You decide what it is allowed to do, and you approve changes before they are written.
6
+
7
+ - **Bring your own AI.** Rynster is not an AI model and does not need an AI subscription of its own.
8
+ - **You set the rules.** Reading, editing, and running commands each have their own permission.
9
+ - **You review every change.** Edits show a diff and can require your approval in the terminal.
10
+ - **Secrets stay protected.** Common credential files and sensitive folders are blocked by default.
11
+ - **Git-aware.** Check status and diffs, and keep an assistant's work in an isolated worktree.
12
+
13
+ > **Important:** Rynster is not a sandbox. Project scripts run with your user's file and network access. Review code before approving any command. See [Local security policy](SECURITY.md).
14
+
15
+ ---
16
+
17
+ ## Quick start
18
+
19
+ ### 1. Install
20
+
21
+ Requires Node.js 22.17+ (22.x) or 24.2+ (24.x), and npm 11+.
22
+
23
+ ```bash
24
+ npm install -g rynster
25
+ rynster --version
26
+ ```
27
+
28
+ To install from a downloaded release file instead, run `npm install -g ./rynster-0.1.1.tgz`.
29
+
30
+ ### 2. Register a project
31
+
32
+ Use your own project name and the absolute path to the project folder.
33
+
34
+ ```bash
35
+ rynster register my-app /path/to/my-app
36
+ rynster projects # list registered projects
37
+ ```
38
+
39
+ ### 3. Connect your AI client
40
+
41
+ Pick the option your client supports.
42
+
43
+ #### Option A: Local (stdio)
44
+
45
+ Best when your client can launch a local command. No tunnel needed.
46
+
47
+ ```bash
48
+ rynster start my-app --stdio
49
+ ```
50
+
51
+ Most clients take a config like this (the file location depends on the client):
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "rynster": {
57
+ "command": "rynster",
58
+ "args": ["start", "my-app", "--stdio"]
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ #### Option B: Remote (HTTPS)
65
+
66
+ Use this when the client has to reach Rynster over the internet. You need your own HTTPS tunnel, since Rynster does not include one.
67
+
68
+ 1. Start a tunnel to local port 3333. For example, with Cloudflare:
69
+ ```bash
70
+ cloudflared tunnel --url http://127.0.0.1:3333
71
+ ```
72
+ 2. Start Rynster with the HTTPS address your tunnel gave you:
73
+ ```bash
74
+ rynster start my-app --port 3333 --public-url https://your-tunnel-hostname
75
+ ```
76
+ 3. In your AI client, add a connector pointing to:
77
+ ```
78
+ https://your-tunnel-hostname/mcp
79
+ ```
80
+ 4. Approve the consent screen when your client opens it.
81
+
82
+ Keep your computer, the tunnel, and Rynster running while the client needs access. If the tunnel hostname changes, restart Rynster with the new `--public-url` and reconnect the client.
83
+
84
+ Authentication is always required over HTTP. A tunnel encrypts traffic but does not replace Rynster's own checks. Never expose an unauthenticated server.
85
+
86
+ Having trouble connecting? Run a read-only check:
87
+
88
+ ```bash
89
+ rynster doctor --url https://your-tunnel-hostname/mcp
90
+ ```
91
+
92
+ ---
93
+
94
+ ## What the assistant can do
95
+
96
+ | Area | Tools |
97
+ | :-- | :-- |
98
+ | Project info | `project_info`, `project_tree`, `project_list` |
99
+ | Read and search | `project_read`, `project_search` |
100
+ | Git review | `git_status`, `git_diff` |
101
+ | Editing sessions | `session_start`, `session_list`, `session_stop` |
102
+ | File changes | `project_patch` |
103
+ | Project checks | `project_typecheck`, `project_lint`, `project_test`, `project_format`, `project_build` |
104
+
105
+ Having a tool does not mean every action is allowed. Each one is checked against your permissions and approval rules.
106
+
107
+ ---
108
+
109
+ ## Permissions and approvals
110
+
111
+ Each action is set to **allow**, **ask** (you approve it in the terminal), or **deny**. Typical defaults:
112
+
113
+ | Action | Default |
114
+ | :-- | :-- |
115
+ | Read and search | Allow |
116
+ | Write and create files | Ask |
117
+ | Run checks (test, lint, build...) | Ask |
118
+ | Git commit | Ask |
119
+ | Delete files | Deny |
120
+ | Git push | Deny |
121
+
122
+ Settings live in `agent.config.json` in your working directory (use `--config` to pick another file). A good starting point is the `agent.config.example.json` file included in the package.
123
+
124
+ When asked to approve an edit, the terminal prompt accepts:
125
+
126
+ - `y` approve
127
+ - `n` reject
128
+ - `d` show the full diff
129
+ - `a` allow writes for this session
130
+
131
+ If no terminal is available (a headless run), anything that needs approval is denied.
132
+
133
+ Leave delete and push on **deny** unless you understand the risk.
134
+
135
+ ### Project commands
136
+
137
+ Assistants can only run the five command types above, using commands you set in `agent.config.json`:
138
+
139
+ ```json
140
+ {
141
+ "commands": {
142
+ "test": { "file": "npm", "args": ["test"] },
143
+ "typecheck": { "file": "npm", "args": ["run", "typecheck"] }
144
+ }
145
+ }
146
+ ```
147
+
148
+ For multi-step commands, write a script you have reviewed and point to it. Shell operators such as `&&`, pipes, and redirects are rejected.
149
+
150
+ ---
151
+
152
+ ## How Rynster keeps your project safe
153
+
154
+ - **Stays inside the project.** Paths are checked against the registered folder. Attempts to escape it, including through symlinks, are blocked.
155
+ - **Protects secrets.** Files like `.env*`, `*.pem`, `*.key`, and `id_rsa*`, plus folders like `.ssh`, `.aws`, and `.git`, are off limits. Add your own with `protectedFiles` in the config.
156
+ - **Detects changed files.** Edits are checked against the version the assistant read, and again after you approve. If the file changed in between, the edit is rejected.
157
+ - **Isolated worktrees.** Sessions can work in their own Git branch (`agent/<session-id>`) so your main branch is untouched.
158
+ - **Private audit log.** Operations are recorded without source code, command arguments, or tokens.
159
+
160
+ These reduce risk but do not make untrusted code safe to run. The full details are in [Local security policy](SECURITY.md).
161
+
162
+ ---
163
+
164
+ ## Managing connected clients
165
+
166
+ See which clients have been granted access, and revoke one:
167
+
168
+ ```bash
169
+ rynster oauth grants
170
+ rynster oauth revoke FULL-GRANT-ID
171
+ ```
172
+
173
+ Copy the full grant ID from the list. Revoking blocks that client right away, including while Rynster is running. The client has to ask for your approval again to get back in. Add `--json` to the list command for machine-readable output.
174
+
175
+ Grants are stored in `~/.rynster/oauth.json`. To use a different file, pass `--storage` or set `RYNSTER_OAUTH_STORAGE_PATH`.
176
+
177
+ Useful settings:
178
+
179
+ | Setting | Purpose |
180
+ | :-- | :-- |
181
+ | `--config` | Choose the project config file |
182
+ | `--public-url` / `RYNSTER_PUBLIC_URL` | Public HTTPS address for remote mode |
183
+ | `RYNSTER_OAUTH_STORAGE_PATH` | Custom location for OAuth grants |
184
+ | `rynster token` | Show the owner token (treat it like a password) |
185
+
186
+ ---
187
+
188
+ ## Troubleshooting
189
+
190
+ **`rynster` is not found after installing.**
191
+ Make sure npm's global bin folder is on your `PATH`, open a new terminal, and run `rynster --version`.
192
+
193
+ **Search does not work, or a ripgrep package is missing.**
194
+ Reinstall with optional dependencies on the machine you will run it on:
195
+
196
+ ```bash
197
+ npm install -g rynster --include=optional
198
+ ```
199
+
200
+ If it still fails, check that your npm registry or proxy provides the `@vscode/ripgrep-*` packages. Do not copy `node_modules` between different operating systems or CPU types.
201
+
202
+ **Remote connection stopped working.**
203
+ Your tunnel hostname probably changed. Restart Rynster with the new `--public-url` and recreate the connection in your client.
204
+
205
+ **An action is denied or seems stuck.**
206
+ Check the permission for that action in your config. `ask` needs you to approve in the terminal, and headless runs deny it.
207
+
208
+ ---
209
+
210
+ ## Known limitations
211
+
212
+ - Runs on your computer only. There is no hosted version in this release.
213
+ - No built-in tunnel for remote access.
214
+ - Not a sandbox. Scripts run with your user's access.
215
+ - Automated security tests cover Linux and Windows. macOS is not yet verified.
216
+ - On Windows, stopping a command may not end every child process it started.
217
+ - Earlier live testing with Claude and ChatGPT used development builds under a previous package name. Token renewal and other lifecycle checks with the current package are still pending.
218
+ - Supports MCP protocol version `2026-07-28` only. Check that your client supports it.
219
+
220
+ ---
221
+
222
+ ## Security
223
+
224
+ To report a vulnerability, email Baskar Ellangudi at **baskar12ai@gmail.com** with the subject `Rynster security report`. Include your Rynster and Node versions, operating system, steps to reproduce, and the impact. Please do not send tokens, credentials, or private files. You do not need GitHub access.
225
+
226
+ Full details: [Local security policy](SECURITY.md).
227
+
228
+ ## License
229
+
230
+ Rynster is proprietary and free for personal and internal business use, including commercial work and client projects. Redistributing, reselling, modifying, or offering it as a hosted service requires written permission. Being on npm does not make it open source. See [LICENSE](LICENSE).
231
+
232
+ ## Support
233
+
234
+ Support is optional. Run `rynster support` or visit [ko-fi.com/bas12](https://ko-fi.com/bas12).
235
+
236
+ ## Release notes
237
+
238
+ See [CHANGELOG.md](CHANGELOG.md).
package/SECURITY.md ADDED
@@ -0,0 +1,84 @@
1
+ # Rynster security policy
2
+
3
+ Reviewed on 2026-10-23. Rynster runs on your own computer; there is no hosted service in this release.
4
+
5
+ ## In plain terms
6
+
7
+ - Rynster trusts you (the operating-system user) and the project code you approve.
8
+ - It is not a sandbox. Approved scripts run with your full file and network access.
9
+ - It guards against an assistant escaping the project, reading secrets, or editing files you did not approve. It does not defend against another malicious process already running as you.
10
+
11
+ ## Reporting a vulnerability
12
+
13
+ Email **Baskar Ellangudi** privately at **[baskar12ai@gmail.com](mailto:baskar12ai@gmail.com)**
14
+ to report a suspected security vulnerability. This is the public reporting contact
15
+ for Rynster; the source repository is private, so reports do not require GitHub
16
+ access. Use the subject `Rynster security report`.
17
+
18
+ Include the installed Rynster version, Node.js version, operating system and
19
+ architecture, affected command or MCP tool, steps to reproduce, expected and
20
+ observed behavior, and the potential impact. Prefer a minimal reproduction using
21
+ temporary files and dummy credentials. Redact logs and configuration before
22
+ sharing them: do not send access or refresh tokens, owner credentials, client
23
+ secrets, private keys, or private project files. Coordinate any sensitive follow-up
24
+ material with the maintainer by email.
25
+
26
+ **Supported versions:** security fixes are provided for the latest 0.1.x release.
27
+
28
+ **Response time:** reports are acknowledged within 5 business days. Confirmed issues are fixed in a new release, and reporters are credited if they wish.
29
+
30
+ ## Authorization and editing sessions
31
+
32
+ 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.
33
+
34
+ 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.
35
+
36
+ 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.
37
+
38
+ ## File and credential protection
39
+
40
+ 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.
41
+
42
+ 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.
43
+
44
+ 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. Rynster's threat model trusts the OS user and approved project code. macOS remains unverified.
45
+
46
+ 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 is 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.
47
+
48
+ 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. Applying and verifying these ACLs requires the built-in Windows PowerShell and the system `whoami` and `icacls` tools. 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.
49
+
50
+ 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.
51
+
52
+ ## Approval and command execution
53
+
54
+ 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.
55
+
56
+ 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. 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.
57
+
58
+ 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.
59
+
60
+ 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: stopping a command may not end every child process it started.
61
+
62
+ **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.
63
+
64
+ ## HTTP and logging
65
+
66
+ 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.
67
+
68
+ ## Managing OAuth grants
69
+
70
+ Owners can inspect credential-free grant metadata with `rynster oauth grants`
71
+ and revoke an exact grant ID with `rynster oauth revoke <grant-id>`. The commands use
72
+ the same default OAuth store and `RYNSTER_OAUTH_STORAGE_PATH` override as the server,
73
+ with an explicit `--storage` option. Listings do not write, migrate, or change file
74
+ permissions. Revocations use the existing writer lock and atomic private save,
75
+ reject unknown IDs and invalid storage, and remain effective after restart.
76
+ The commands are not exposed as MCP tools and do not revoke the separate owner
77
+ bearer credential. Client labels in terminal output are sanitized.
78
+
79
+ ## Verification status
80
+
81
+ - **Automated tests:** Linux and Windows, covering HTTP/stdio authorization, grant and refresh ownership, physical root replacement, credential guards, stale/canceled/expired approvals, symlink replacement, approved edits, headless denial, command grammar/environment/output/process cleanup, configuration races, and targeted grant revocation. Installed-package checks cover production-only dependency resolution, CLI configuration, native search assets, workflow handles, secret guards, headless command denial, protocol rejection, and shutdown.
82
+ - **macOS:** unverified.
83
+ - **Live client testing:** earlier development builds were tested with Claude and ChatGPT (onboarding, reads, revocation, token renewal). The renamed package has not been re-tested live, and ChatGPT lifecycle checks are pending. Passing automated tests is not live-client evidence.
84
+ - **Not provided:** a hosted service, an authenticated headless approval channel, and an OS-isolated command executor.
@@ -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=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[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 &amp; 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.1",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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
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
+ }