@pencere/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@pencere/cli` are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package follows
5
+ [semantic versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-10-21
10
+
11
+ ### Added
12
+
13
+ - `pencere run [--tool <name>] [--args <json>] [--view <dir>] -- <command> [args...]`: run an MCP
14
+ server that speaks stdio, written in any language, as a pencere program. The view opens in the
15
+ terminal pane `pencere run` runs in, and closes with ⌃C or when the view or the terminal ends
16
+ it. Without `--tool` the server is started once to ask which tool names a view; `--view` shows
17
+ the view from a directory's files, sent to the terminal once.
18
+ - The server runs with the program's environment, in a process group of its own so that the
19
+ terminal's ⌃C reaches `pencere run` and not the server, and is ended by closing its stdin, then
20
+ SIGTERM and SIGKILL to its group. What it wrote to stderr is shown on the screen the person gets
21
+ back when it failed.
22
+ - `pencere dev [--view <dir>] [--watch <path>]... [--tool <name>] [--args <json>] -- <command>
23
+ [args...]`: `pencere run`, and when the files of the view (`--view`) or the files the server reads
24
+ (`--watch`, a file or a directory, as often as needed) change, the view closes, the server is
25
+ started again and the view opens again in the same pane. Changes that come together are one
26
+ reopening. The terminal keeps the view's files by content, so only the files that changed cross
27
+ the stream. A change that leaves the files as they were, or a build that has not finished writing
28
+ the view, shows nothing; a terminal that cannot reopen a view is told of once, after the view has
29
+ closed, and the view runs as it does under `pencere run`.
30
+ - Exit status: 0 the view ran and closed, 1 it could not run, 2 a mistake in the command line,
31
+ 3 the terminal does not show pencere views; ⌃C, SIGTERM and a hangup (SIGHUP, ssh going away) end
32
+ the view and the server, and then the process by the same signal (130, 143, 129). A terminal that
33
+ stopped answering while the view was shown again is a 1 with a message of its own, not a 3.
34
+ - A server that ends while `pencere dev` runs (a mistake saved in it) is not the end of `pencere
35
+ dev`: the terminal is given back, the end of what the server wrote to stderr is shown with what
36
+ happened, and the next change starts the server and the view again; ⌃C ends it. `pencere run`
37
+ still ends with the server.
38
+ - Which tool to show is found as the terminal would find it: a tool names a view by a `ui://`
39
+ resource only, and the tool list is read a page at a time up to the terminal's limit of 16 pages.
40
+ What a server wrote (tool names, an error message) is shown as text.
41
+ - A server that leaves on its own, at the end of its stdin or by falling, leaves what it started
42
+ behind in its group, and that is ended too (SIGTERM, then SIGKILL).
43
+ - A terminal that goes away (ssh dropped) fails what is written to and read from it: that is not an
44
+ error that ends `pencere`, which is busy letting go of the server.
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,2 @@
1
+ pencere
2
+ Copyright 2026 The pencere Authors
package/README.md CHANGED
@@ -1,3 +1,134 @@
1
- # Temporary Holding Version
1
+ # @pencere/cli
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
+ Run an MCP server written in any language as a pencere program, so its MCP App view opens
4
+ in the terminal pane you ran it in.
5
+
6
+ ```sh
7
+ npx @pencere/cli run -- python server.py
8
+ npx @pencere/cli dev --view dist/view -- node dist/server.js
9
+ ```
10
+
11
+ [pencere](https://github.com/bateri/pencere) shows an MCP App view in the terminal pane a
12
+ program runs in, on your machine or on a server over plain ssh. The program is an MCP server,
13
+ and `@pencere/sdk` is how a TypeScript one asks the terminal for a view. `pencere run` is for
14
+ every other server: Python, Go, Rust, a shell script, anything that speaks MCP over stdio. It
15
+ runs the server as an MCP client would, talks pencere's protocol with the terminal, and carries
16
+ the messages between the two. The server is the one you would give Claude Desktop or VS Code,
17
+ unchanged. `pencere dev` is the same while you work on the view or the server: when their files
18
+ change, the view is shown again.
19
+
20
+ ## Use
21
+
22
+ ```
23
+ pencere run [options] -- <command> [args...]
24
+ ```
25
+
26
+ Everything after `--` is the server's command, taken as it is.
27
+
28
+ | Option | What it does |
29
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | `--tool <name>` | The tool whose view to show. Without it, `pencere run` starts the server once to ask (`initialize` and `tools/list`) and takes the one tool whose `_meta.ui.resourceUri` names a view. |
31
+ | `--args <json>` | The arguments the tool is called with, a JSON object. |
32
+ | `--view <dir>` | Show the view from the files in `<dir>` (it needs an `index.html`). They cross the terminal's stream once and the terminal keeps them by content. Without it the terminal reads the server's `ui://` resource. |
33
+ | `-h`, `--help` | Usage. |
34
+ | `-v`, `--version` | The version. |
35
+
36
+ The same options and exit status are `pencere dev`'s, which adds `--watch` (see below).
37
+
38
+ If the server has no tool with a view, or several, `pencere run` says which tools there are
39
+ and, for several, asks you to choose one with `--tool`. A tool names a view by a `ui://`
40
+ resource, as the terminal reads it, and the tool list is read a page at a time as far as the
41
+ terminal reads it (16 pages). What the server wrote is shown as text.
42
+
43
+ A terminal that does not host pencere is told so: `pencere run` says that the command is an MCP
44
+ server, which an MCP client can take as it is, and ends. A server is not a text interface, so it
45
+ is not attached to the terminal, and with `--tool` it is not started for it at all (without
46
+ `--tool` it was started once, to ask which tool has a view).
47
+
48
+ ### Exit status
49
+
50
+ | Status | Meaning |
51
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `0` | The view ran and closed (you closed it, the view did, or the program did). |
53
+ | `1` | It could not run: the server did not start, did not answer as an MCP server, or ended while its view was up; the terminal refused or ended the view, or stopped answering while the view was shown again. |
54
+ | `2` | A mistake in the command line, or a `--view` directory that is not a view, or several tools with a view and no `--tool`. |
55
+ | `3` | This terminal does not show pencere views, or the input or output of `pencere run` is not a terminal. |
56
+ | ⌃C, `SIGTERM`, a hangup | The view and the server are ended first, then `pencere run` ends by the same signal, so the shell reads 130, 143 or 129 (the terminal went away, as when ssh does). |
57
+
58
+ When the server fails, what it wrote to stderr is shown on the screen you get back. While a
59
+ view is up the screen is the view's, so the end of it is kept (the last 16 KiB) and printed
60
+ after the view has closed, as text.
61
+
62
+ ## Reloading with `pencere dev`
63
+
64
+ ```
65
+ pencere dev [options] -- <command> [args...]
66
+ ```
67
+
68
+ ```sh
69
+ pencere dev --view dist/view -- node dist/server.js # a view that is built into dist/view
70
+ pencere dev --watch server.py -- python server.py # a server whose own files are the work
71
+ ```
72
+
73
+ `pencere dev` is `pencere run`, and it watches files. When they change, the view in the pane
74
+ closes, the server is started again, and the view opens again, in the same pane and without
75
+ leaving the alternate screen. What the view held in its own memory is gone, as it is when the
76
+ terminal reconnects: its state is rebuilt from the server's tool results, as at the first open.
77
+
78
+ | Option | What it watches |
79
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | `--view <dir>` | The view's files. A change that leaves the files as they were (a save that wrote the same bytes) shows nothing; the terminal is sent only what changed. |
81
+ | `--watch <path>` | A file or directory the server reads, as often as needed. A change to what it holds (size or modification time) starts the server and the view again. |
82
+
83
+ One of the two is needed. Names that begin with a dot, `node_modules` and `__pycache__` are not
84
+ watched under a directory: a server that writes into a directory it is watched through would end
85
+ every run with a change.
86
+
87
+ - **A burst is one reopening.** A build writes many files; the view is reopened once they have
88
+ been quiet for a moment, and a change that comes while it is reopening is shown after it.
89
+ - **A half-built view is not shown.** A build that has emptied its output directory and not yet
90
+ written `index.html` leaves something that is not a view. The change is looked at again at the
91
+ next one, which is the build writing more.
92
+ - **The terminal does the swap.** The view is closed and opened by the terminal's own messages
93
+ (`dismiss`; specification §10.8), which is why this works when the server runs over plain ssh
94
+ and the terminal is on the other side: the files cross the stream, not a port. A terminal that
95
+ does not list `dismiss` in its `welcome` cannot reopen a view: `pencere dev` runs the view as
96
+ `pencere run` would, and says after the view has closed that the changes were not shown.
97
+ - **What it tells you is told after.** While the view is up the screen is the view's, so notes
98
+ (a view that could not be read at the last change, a watch that failed) are printed on the
99
+ screen you get back, as the server's stderr is.
100
+ - **A server that ends is waited out.** If the server exits while its view is up or opening, as
101
+ after a syntax error is saved, the view goes with it and the terminal is yours again: `pencere dev`
102
+ says that the server ended, shows the end of what it wrote to stderr, and waits. The next change
103
+ to the files it watches starts the server and the view again, and nothing has to be run twice.
104
+ A change that leaves the files as they were is not another try. ⌃C ends `pencere dev` as ever.
105
+ `pencere run`, which watches nothing, ends with the server.
106
+
107
+ ## How the server is run
108
+
109
+ - **As an MCP client runs it.** The command is started with your whole environment and working
110
+ directory, with pipes for stdin and stdout; stdout carries one JSON-RPC message to a line.
111
+ Each view is a new MCP session and so a new process: a stdio server expects one `initialize`
112
+ for the life of its process, and a view that the terminal starts again (after a reconnection)
113
+ or that the program reopens gets a server of its own.
114
+ - **In a process group of its own.** ⌃C is the terminal's signal to the foreground group, which
115
+ is `pencere run`'s. The server must not take it: `pencere run` ends it, after telling the
116
+ terminal the program is done. It closes the server's stdin first, which every stdio server
117
+ takes as the end, then sends SIGTERM and SIGKILL to the whole group, which catches what a
118
+ wrapper (`npx`, `uv run`, `sh -c`) started. A server that leaves at the end of its stdin, or
119
+ falls, leaves what it started behind in the group, and that gets the same two signals.
120
+ - **Nothing else is added.** What the terminal asks (the question, the token, the bundle's
121
+ files, ⌃C and `bye`, starting the view again) is done by `@pencere/sdk`'s `serve`, which this
122
+ package runs; `pencere run` does not parse the server's messages.
123
+
124
+ Node 22 or later is needed, on a system with `stty` (macOS, Linux): the SDK sets the terminal's
125
+ modes with it, so Windows has no host as far as it is concerned.
126
+
127
+ ## Versions
128
+
129
+ This release speaks version 1 of the pencere wire (a draft), through `@pencere/sdk`. The package
130
+ follows semantic versioning; [CHANGELOG.md](CHANGELOG.md) says what each release changed.
131
+
132
+ ## License
133
+
134
+ Apache-2.0 — see [LICENSE](LICENSE).
package/dist/args.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The command line: `pencere run [options] -- <command> [args...]` and
3
+ * `pencere dev [options] -- <command> [args...]`.
4
+ *
5
+ * Everything after the first `--` is the server's command, taken as it is: a
6
+ * server's own flags are not pencere's to read. What comes before it is read with
7
+ * `node:util`'s `parseArgs`, strictly, so a mistyped option is an error and not a
8
+ * flag handed to nobody.
9
+ */
10
+ export declare const USAGE = "usage: pencere run [options] -- <command> [args...]\n pencere dev [options] -- <command> [args...]\n\nrun: runs <command>, an MCP server that speaks stdio and is written in any\nlanguage, and shows its view in this terminal pane. The server is started as an\nMCP client would start it, so it is the same server an MCP Apps host runs.\n\n pencere run -- python server.py\n pencere run --tool list_containers --view ./view -- node dist/server.js\n\ndev: the same, and the view is shown again when its files change (--view) or\nwhen the server's own files do (--watch): the view closes, the server is\nstarted again and the view opens again, with only the files that changed sent.\n\n pencere dev --view dist/view -- node dist/server.js\n pencere dev --watch server.py -- python server.py\n\nA server that ends while dev runs (a mistake saved in it) is told of, with the end\nof what it wrote to stderr, and tried again at the next change; ^C ends dev.\n\noptions:\n --tool <name> the tool whose view to show; without it, the one tool of the\n server that names a view (_meta.ui.resourceUri)\n --args <json> the arguments the tool is called with, a JSON object\n --view <dir> show the view from the files in <dir> (sent to the terminal\n once, then kept by content) instead of the server's own\n ui:// resource\n --watch <path> dev only, repeatable: a file or directory whose change starts\n the server and the view again; --view's directory is watched\n already, and one of --view and --watch is needed\n -h, --help this text\n -v, --version the version of pencere\n\nexit status: 0 the view ran and closed; 1 it could not run (the server failed to\nstart or ended, the terminal refused or ended the view, or stopped answering\nwhile the view was shown again); 2 a mistake in the command line; 3 this terminal\ndoes not show pencere views; 130, 143 and 129 after ^C, SIGTERM and a hangup.\n";
11
+ /** The server to run and the view to show, which `run` and `dev` are both given. */
12
+ export interface Launch {
13
+ readonly command: string;
14
+ readonly args: readonly string[];
15
+ readonly tool?: string;
16
+ readonly arguments?: Record<string, unknown>;
17
+ readonly view?: string;
18
+ }
19
+ /** What the command line asks for. */
20
+ export type Command = {
21
+ readonly kind: "help";
22
+ } | {
23
+ readonly kind: "version";
24
+ } | ({
25
+ readonly kind: "run";
26
+ } & Launch) | ({
27
+ readonly kind: "dev";
28
+ /** The files whose change starts the server and the view again, besides the view's directory. */
29
+ readonly watch: readonly string[];
30
+ } & Launch);
31
+ /** A mistake in the command line: what it is, so that the person can fix it. */
32
+ export declare class UsageError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ /** @throws {UsageError} for anything that is not a command of pencere. */
36
+ export declare function parse(argv: readonly string[]): Command;
package/dist/args.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The command line: `pencere run [options] -- <command> [args...]` and
3
+ * `pencere dev [options] -- <command> [args...]`.
4
+ *
5
+ * Everything after the first `--` is the server's command, taken as it is: a
6
+ * server's own flags are not pencere's to read. What comes before it is read with
7
+ * `node:util`'s `parseArgs`, strictly, so a mistyped option is an error and not a
8
+ * flag handed to nobody.
9
+ */
10
+ import { parseArgs } from "node:util";
11
+ export const USAGE = `\
12
+ usage: pencere run [options] -- <command> [args...]
13
+ pencere dev [options] -- <command> [args...]
14
+
15
+ run: runs <command>, an MCP server that speaks stdio and is written in any
16
+ language, and shows its view in this terminal pane. The server is started as an
17
+ MCP client would start it, so it is the same server an MCP Apps host runs.
18
+
19
+ pencere run -- python server.py
20
+ pencere run --tool list_containers --view ./view -- node dist/server.js
21
+
22
+ dev: the same, and the view is shown again when its files change (--view) or
23
+ when the server's own files do (--watch): the view closes, the server is
24
+ started again and the view opens again, with only the files that changed sent.
25
+
26
+ pencere dev --view dist/view -- node dist/server.js
27
+ pencere dev --watch server.py -- python server.py
28
+
29
+ A server that ends while dev runs (a mistake saved in it) is told of, with the end
30
+ of what it wrote to stderr, and tried again at the next change; ^C ends dev.
31
+
32
+ options:
33
+ --tool <name> the tool whose view to show; without it, the one tool of the
34
+ server that names a view (_meta.ui.resourceUri)
35
+ --args <json> the arguments the tool is called with, a JSON object
36
+ --view <dir> show the view from the files in <dir> (sent to the terminal
37
+ once, then kept by content) instead of the server's own
38
+ ui:// resource
39
+ --watch <path> dev only, repeatable: a file or directory whose change starts
40
+ the server and the view again; --view's directory is watched
41
+ already, and one of --view and --watch is needed
42
+ -h, --help this text
43
+ -v, --version the version of pencere
44
+
45
+ exit status: 0 the view ran and closed; 1 it could not run (the server failed to
46
+ start or ended, the terminal refused or ended the view, or stopped answering
47
+ while the view was shown again); 2 a mistake in the command line; 3 this terminal
48
+ does not show pencere views; 130, 143 and 129 after ^C, SIGTERM and a hangup.
49
+ `;
50
+ /** A mistake in the command line: what it is, so that the person can fix it. */
51
+ export class UsageError extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "UsageError";
55
+ }
56
+ }
57
+ /** The arguments of a tool call: one JSON object. */
58
+ function toolArguments(text) {
59
+ let value;
60
+ try {
61
+ value = JSON.parse(text);
62
+ }
63
+ catch (error) {
64
+ throw new UsageError(`--args is not JSON: ${error instanceof Error ? error.message : error}`);
65
+ }
66
+ if (typeof value !== "object" || value === null || Array.isArray(value))
67
+ throw new UsageError("--args must be a JSON object");
68
+ return value;
69
+ }
70
+ const COMMANDS = "use `pencere run [options] -- <command> [args...]` or `pencere dev ...`";
71
+ /** @throws {UsageError} for anything that is not a command of pencere. */
72
+ export function parse(argv) {
73
+ const [name, ...rest] = argv;
74
+ if (name === undefined)
75
+ throw new UsageError(`no command: ${COMMANDS}`);
76
+ if (name === "-h" || name === "--help" || name === "help")
77
+ return { kind: "help" };
78
+ if (name === "-v" || name === "--version")
79
+ return { kind: "version" };
80
+ if (name !== "run" && name !== "dev")
81
+ throw new UsageError(`unknown command "${name}": ${COMMANDS}`);
82
+ // The server's command is everything after the first `--`.
83
+ const dash = rest.indexOf("--");
84
+ const own = dash === -1 ? rest : rest.slice(0, dash);
85
+ let parsed;
86
+ try {
87
+ parsed = parseOwn(own);
88
+ }
89
+ catch (error) {
90
+ throw new UsageError(error instanceof Error ? error.message : String(error));
91
+ }
92
+ const { values, positionals } = parsed;
93
+ if (values.help === true)
94
+ return { kind: "help" };
95
+ const [stray] = positionals;
96
+ if (stray !== undefined)
97
+ throw new UsageError(`unexpected "${stray}": the server's command goes after --`);
98
+ if (dash === -1)
99
+ throw new UsageError("no command: put the server's command after --");
100
+ const [command, ...args] = rest.slice(dash + 1);
101
+ if (command === undefined || command === "")
102
+ throw new UsageError("no command after --");
103
+ for (const option of ["tool", "view"])
104
+ if (values[option] === "")
105
+ throw new UsageError(`--${option} needs a value`);
106
+ const watch = values.watch ?? [];
107
+ if (watch.includes(""))
108
+ throw new UsageError("--watch needs a value");
109
+ const launch = {
110
+ command,
111
+ args,
112
+ ...(values.tool === undefined ? {} : { tool: values.tool }),
113
+ ...(values.args === undefined ? {} : { arguments: toolArguments(values.args) }),
114
+ ...(values.view === undefined ? {} : { view: values.view }),
115
+ };
116
+ if (name === "run") {
117
+ if (values.watch !== undefined)
118
+ throw new UsageError("--watch is for `pencere dev`: `pencere run` shows the view once");
119
+ return { kind: "run", ...launch };
120
+ }
121
+ if (launch.view === undefined && watch.length === 0)
122
+ throw new UsageError("nothing to watch: give --view <dir> for the view's files, or --watch <path> for the server's own");
123
+ return { kind: "dev", watch, ...launch };
124
+ }
125
+ function parseOwn(args) {
126
+ return parseArgs({
127
+ args: [...args],
128
+ options: {
129
+ tool: { type: "string" },
130
+ args: { type: "string" },
131
+ view: { type: "string" },
132
+ watch: { type: "string", multiple: true },
133
+ help: { type: "boolean", short: "h" },
134
+ },
135
+ allowPositionals: true,
136
+ strict: true,
137
+ });
138
+ }
139
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The program's MCP server, stood in for by a process.
3
+ *
4
+ * `serve` of the SDK takes anything that can `connect` a transport and `close`
5
+ * the session, and does everything else of a pencere program: the question to the
6
+ * terminal, the token, the bundle, ⌃C and `bye`, the host starting the view again
7
+ * (§10.7) and the program reopening it (§10.8). It connects once for each view it
8
+ * opens. This is that server for a program that has none of its own: it starts the
9
+ * command for each connection and carries the JSON-RPC messages between the
10
+ * terminal's stream and the command's stdio, line by line, untouched. A server
11
+ * that has been connected to a view once has done its `initialize`; the next view
12
+ * is a new session, and so a new process, which is also what a stdio server
13
+ * expects: one `initialize` for the life of the process.
14
+ */
15
+ import type { Transport } from "@modelcontextprotocol/server";
16
+ import type { Connectable } from "@pencere/sdk";
17
+ import type { CloseTiming, Exit, ServerCommand } from "./server-process.js";
18
+ import type { Tail } from "./tail.js";
19
+ export interface ConnectorOptions {
20
+ /** What the server writes to stderr goes here, as do the connector's own notes on the stream. */
21
+ readonly tail: Tail;
22
+ /**
23
+ * The server's process ended, and the connector had not asked it to. The view
24
+ * has nothing to talk to any more: the caller ends it.
25
+ */
26
+ readonly unexpectedExit: (exit: Exit) => void;
27
+ readonly timing?: CloseTiming;
28
+ }
29
+ export declare class ProcessConnector implements Connectable {
30
+ #private;
31
+ constructor(spec: ServerCommand, options: ConnectorOptions);
32
+ /** How many processes this connector has started: one for each view that was opened. */
33
+ get starts(): number;
34
+ /** @throws {StartError} when the command cannot be started. */
35
+ connect(transport: Transport): Promise<void>;
36
+ /** Ends the process of the session that is connected, if there is one, and resolves when it is gone. */
37
+ close(): Promise<void>;
38
+ /**
39
+ * Resolves when every process this connector started has ended. `serve` lets go of
40
+ * a session without waiting for it (a signal must not wait for a stubborn server),
41
+ * and a program that is about to leave has to.
42
+ */
43
+ drain(): Promise<void>;
44
+ }