runpane 2.3.0__tar.gz → 2.3.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runpane
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Thin PyPI installer and remote setup CLI for Pane
5
5
  Author-email: Dcouple Inc <hello@dcouple.ai>
6
6
  License: AGPL-3.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "runpane"
7
- version = "2.3.0"
7
+ version = "2.3.2"
8
8
  description = "Thin PyPI installer and remote setup CLI for Pane"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -0,0 +1 @@
1
+ __version__ = "2.3.2"
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from .generated_contract import RUNPANE_CONTRACT
7
+
8
+
9
+ def run_agent_context(parsed: Any) -> int:
10
+ result = build_agent_context_result(parsed.context_command)
11
+ if parsed.json:
12
+ print(json.dumps(result, indent=2))
13
+ return 0
14
+
15
+ if result["mode"] == "brief":
16
+ print(render_brief(result))
17
+ else:
18
+ print(render_command_detail(result["command"]))
19
+ return 0
20
+
21
+
22
+ def build_agent_context_result(command_name: Optional[str] = None) -> Dict[str, Any]:
23
+ if command_name:
24
+ return {
25
+ "ok": True,
26
+ "mode": "command",
27
+ "source": "runpane-contract",
28
+ "command": get_command_detail(command_name),
29
+ }
30
+
31
+ brief = RUNPANE_CONTRACT["agentContext"]["brief"]
32
+ return {
33
+ "ok": True,
34
+ "mode": "brief",
35
+ "source": "runpane-contract",
36
+ "summary": brief["summary"],
37
+ "rules": brief["rules"],
38
+ "tools": brief["tools"],
39
+ "detailCommand": brief["detailCommand"],
40
+ }
41
+
42
+
43
+ def get_command_detail(command_name: str) -> Dict[str, Any]:
44
+ for command in RUNPANE_CONTRACT["agentContext"]["commands"].values():
45
+ if command["name"] == command_name:
46
+ return command
47
+
48
+ raise ValueError(f"Unknown runpane command: {command_name}. Expected one of: {', '.join(command_names())}")
49
+
50
+
51
+ def render_brief(result: Dict[str, Any]) -> str:
52
+ lines = [
53
+ RUNPANE_CONTRACT["agentContext"]["brief"]["title"],
54
+ "",
55
+ result["summary"],
56
+ "",
57
+ "Rules:",
58
+ ]
59
+ lines.extend(f"- {rule}" for rule in result["rules"])
60
+ lines.extend(["", "Tools:"])
61
+ for tool in result["tools"]:
62
+ lines.append(f"- {tool['name']}: {tool['summary']}")
63
+ lines.append(f" Args: {', '.join(tool['arguments'])}")
64
+ lines.extend(["", f"Detailed definitions: {result['detailCommand']}"])
65
+ return "\n".join(lines)
66
+
67
+
68
+ def render_command_detail(command: Dict[str, Any]) -> str:
69
+ lines = [
70
+ f"runpane {command['name']}",
71
+ "",
72
+ command["summary"],
73
+ "",
74
+ "Details:",
75
+ command["details"],
76
+ "",
77
+ f"Requires Pane daemon: {'yes' if command.get('requiresPaneDaemon') else 'no'}",
78
+ f"Mutates Pane state: {'yes' if command.get('mutates') else 'no'}",
79
+ "",
80
+ "Arguments:",
81
+ ]
82
+
83
+ if not command["arguments"]:
84
+ lines.append("- none")
85
+ else:
86
+ for argument in command["arguments"]:
87
+ value = f" {argument['value']}" if argument.get("value") else ""
88
+ required = "required" if argument["required"] else "optional"
89
+ lines.append(f"- {argument['name']}{value} ({required}): {argument['description']}")
90
+
91
+ lines.extend(["", "Examples:"])
92
+ lines.extend(f"- {example}" for example in command["examples"])
93
+
94
+ if command.get("jsonSchemas"):
95
+ lines.extend(["", "JSON schemas:"])
96
+ lines.extend(f"- {schema}" for schema in command["jsonSchemas"])
97
+
98
+ if command.get("notes"):
99
+ lines.extend(["", "Notes:"])
100
+ lines.extend(f"- {note}" for note in command["notes"])
101
+
102
+ return "\n".join(lines)
103
+
104
+
105
+ def command_names() -> List[str]:
106
+ return sorted(
107
+ command["name"]
108
+ for command in RUNPANE_CONTRACT["agentContext"]["commands"].values()
109
+ )
@@ -4,8 +4,9 @@ from dataclasses import dataclass, field
4
4
  import os
5
5
  import socket
6
6
  import sys
7
- from typing import Dict, List, Optional, TypeVar
7
+ from typing import Dict, List, Optional, Tuple, TypeVar
8
8
 
9
+ from .agent_context import run_agent_context
9
10
  from .doctor import run_doctor
10
11
  from .download import download_artifact
11
12
  from .generated_contract import RUNPANE_CONTRACT
@@ -16,19 +17,35 @@ from .installers import (
16
17
  should_reuse_existing_pane,
17
18
  spawn_pane,
18
19
  )
20
+ from .local_control import run_panes_create, run_repos_add, run_repos_list
19
21
  from .platforms import detect_platform
20
22
  from .releases import resolve_release
21
23
  from .version import print_version
22
24
 
23
25
  SOURCE = "pip"
24
26
 
25
- COMMANDS = {command["name"] for command in RUNPANE_CONTRACT["commands"]}
27
+ COMMAND_MATCHERS = sorted(
28
+ ((command["name"], command["name"].split(" ")) for command in RUNPANE_CONTRACT["commands"]),
29
+ key=lambda item: len(item[1]),
30
+ reverse=True,
31
+ )
26
32
  TARGETS = set(RUNPANE_CONTRACT["enums"]["installTargets"])
27
33
  FORMATS = set(RUNPANE_CONTRACT["enums"]["artifactFormats"])
28
34
  CHANNELS = set(RUNPANE_CONTRACT["enums"]["channels"])
35
+ AGENTS = set(RUNPANE_CONTRACT["enums"]["agents"])
29
36
 
30
37
  REMOTE_VALUE_FLAGS = {flag["name"] for flag in RUNPANE_CONTRACT["flags"]["remoteValue"]}
31
38
  REMOTE_BOOLEAN_FLAGS = {flag["name"] for flag in RUNPANE_CONTRACT["flags"]["remoteBoolean"]}
39
+ LOCAL_VALUE_FLAGS = {
40
+ value
41
+ for flag in RUNPANE_CONTRACT["flags"]["localValue"]
42
+ for value in [flag["name"], *flag.get("aliases", [])]
43
+ }
44
+ LOCAL_BOOLEAN_FLAGS = {
45
+ value
46
+ for flag in RUNPANE_CONTRACT["flags"]["localBoolean"]
47
+ for value in [flag["name"], *flag.get("aliases", [])]
48
+ }
32
49
  DEFAULTS = RUNPANE_CONTRACT["defaults"]
33
50
 
34
51
 
@@ -44,6 +61,21 @@ class ParsedArgs:
44
61
  dry_run: bool = DEFAULTS["dryRun"]
45
62
  yes: bool = DEFAULTS["yes"]
46
63
  verbose: bool = DEFAULTS["verbose"]
64
+ json: bool = False
65
+ context_command: Optional[str] = None
66
+ pane_dir: Optional[str] = None
67
+ repo: Optional[str] = None
68
+ repo_path: Optional[str] = None
69
+ name: Optional[str] = None
70
+ worktree_name: Optional[str] = None
71
+ base_branch: Optional[str] = None
72
+ agent: Optional[str] = None
73
+ tool_command: Optional[str] = None
74
+ title: Optional[str] = None
75
+ initial_input: Optional[str] = None
76
+ initial_input_file: Optional[str] = None
77
+ from_json: Optional[str] = None
78
+ timeout_ms: Optional[float] = None
47
79
  help_topic: Optional[str] = None
48
80
  remote_setup_args: List[str] = field(default_factory=list)
49
81
 
@@ -64,6 +96,14 @@ def main(argv: Optional[List[str]] = None) -> int:
64
96
  return print_version(parsed.pane_path)
65
97
  if parsed.command == "doctor":
66
98
  return run_doctor(parsed, SOURCE)
99
+ if parsed.command == "agent-context":
100
+ return run_agent_context(parsed)
101
+ if parsed.command == "repos list":
102
+ return run_repos_list(parsed)
103
+ if parsed.command == "repos add":
104
+ return run_repos_add(parsed)
105
+ if parsed.command == "panes create":
106
+ return run_panes_create(parsed)
67
107
  if parsed.command in {"install", "update"}:
68
108
  return install_or_update(parsed)
69
109
  print(help_text(None))
@@ -192,15 +232,21 @@ def parse_args(argv: List[str]) -> ParsedArgs:
192
232
  args = list(argv)
193
233
  if not args or args[0] in {"-h", "--help"}:
194
234
  return ParsedArgs(command="help")
195
- first = args.pop(0)
235
+ first = args[0]
196
236
  if first in {"-v", "--version"}:
197
237
  return ParsedArgs(command="version")
198
- if first not in COMMANDS:
199
- raise ValueError(f"Unknown command: {first}\n\n{help_text(None)}")
200
238
  if first == "help":
201
- return ParsedArgs(command="help", help_topic=args[0] if args else None)
239
+ args.pop(0)
240
+ return ParsedArgs(command="help", help_topic=" ".join(args) or None)
241
+
242
+ matched = match_command(args)
243
+ if not matched:
244
+ raise ValueError(f"Unknown command: {first}\n\n{help_text(None)}")
202
245
 
203
- parsed = ParsedArgs(command=first)
246
+ command, tokens = matched
247
+ del args[:len(tokens)]
248
+
249
+ parsed = ParsedArgs(command=command)
204
250
  if parsed.command == "install" and args and not args[0].startswith("-"):
205
251
  target = args.pop(0)
206
252
  if target not in TARGETS:
@@ -218,6 +264,8 @@ def parse_flags(args: List[str], parsed: ParsedArgs) -> None:
218
264
  index = 0
219
265
  while index < len(args):
220
266
  arg = args[index]
267
+ is_agent_context_command = parsed.command == "agent-context"
268
+ is_local_command = parsed.command in {"repos list", "repos add", "panes create"}
221
269
  if arg in {"-h", "--help"}:
222
270
  parsed.help_topic = parsed.command
223
271
  parsed.command = "help"
@@ -227,6 +275,16 @@ def parse_flags(args: List[str], parsed: ParsedArgs) -> None:
227
275
  parsed.yes = True
228
276
  elif arg == "--verbose":
229
277
  parsed.verbose = True
278
+ elif is_agent_context_command and arg == "--json":
279
+ parsed.json = True
280
+ elif is_agent_context_command and arg == "--command":
281
+ index += 1
282
+ parsed.context_command = read_value(args, index, arg)
283
+ elif is_local_command and arg in LOCAL_BOOLEAN_FLAGS:
284
+ parse_local_boolean_flag(parsed, arg)
285
+ elif is_local_command and arg in LOCAL_VALUE_FLAGS:
286
+ index += 1
287
+ parse_local_value_flag(parsed, arg, read_value(args, index, arg))
230
288
  elif arg == "--version":
231
289
  index += 1
232
290
  parsed.pane_version = read_value(args, index, arg)
@@ -262,6 +320,71 @@ def parse_flags(args: List[str], parsed: ParsedArgs) -> None:
262
320
  index += 1
263
321
 
264
322
 
323
+ def match_command(args: List[str]) -> Optional[Tuple[str, List[str]]]:
324
+ for command, tokens in COMMAND_MATCHERS:
325
+ if args[:len(tokens)] == tokens:
326
+ return command, tokens
327
+ return None
328
+
329
+
330
+ def parse_local_boolean_flag(parsed: ParsedArgs, flag: str) -> None:
331
+ if flag == "--json":
332
+ parsed.json = True
333
+ return
334
+ raise ValueError(f"Unknown option for {parsed.command}: {flag}")
335
+
336
+
337
+ def parse_local_value_flag(parsed: ParsedArgs, flag: str, value: str) -> None:
338
+ if flag == "--pane-dir":
339
+ parsed.pane_dir = value
340
+ return
341
+ if flag == "--repo":
342
+ parsed.repo = value
343
+ return
344
+ if flag == "--path":
345
+ parsed.repo_path = value
346
+ return
347
+ if flag == "--name":
348
+ parsed.name = value
349
+ return
350
+ if flag == "--worktree-name":
351
+ parsed.worktree_name = value
352
+ return
353
+ if flag == "--base-branch":
354
+ parsed.base_branch = value
355
+ return
356
+ if flag == "--agent":
357
+ if value not in AGENTS:
358
+ raise ValueError(f"Invalid --agent {value}. Expected one of: {', '.join(sorted(AGENTS))}")
359
+ parsed.agent = value
360
+ return
361
+ if flag == "--tool-command":
362
+ parsed.tool_command = value
363
+ return
364
+ if flag == "--title":
365
+ parsed.title = value
366
+ return
367
+ if flag in {"--initial-input", "--prompt"}:
368
+ parsed.initial_input = value
369
+ return
370
+ if flag == "--initial-input-file":
371
+ parsed.initial_input_file = value
372
+ return
373
+ if flag == "--from-json":
374
+ parsed.from_json = value
375
+ return
376
+ if flag == "--timeout-ms":
377
+ try:
378
+ timeout_ms = float(value)
379
+ except ValueError as error:
380
+ raise ValueError("--timeout-ms must be a positive number.") from error
381
+ if timeout_ms <= 0:
382
+ raise ValueError("--timeout-ms must be a positive number.")
383
+ parsed.timeout_ms = timeout_ms
384
+ return
385
+ raise ValueError(f"Unknown option for {parsed.command}: {flag}")
386
+
387
+
265
388
  def append_remote_arg(parsed: ParsedArgs, flag: str, value: Optional[str] = None) -> None:
266
389
  if parsed.command == "install" and parsed.target == "daemon":
267
390
  parsed.remote_setup_args.append(flag)
@@ -272,7 +395,7 @@ def append_remote_arg(parsed: ParsedArgs, flag: str, value: Optional[str] = None
272
395
 
273
396
 
274
397
  def read_value(args: List[str], index: int, flag: str) -> str:
275
- if index >= len(args) or args[index].startswith("-"):
398
+ if index >= len(args) or (args[index].startswith("-") and args[index] != "-"):
276
399
  raise ValueError(f"{flag} requires a value.")
277
400
  return args[index]
278
401
 
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import ntpath
6
+ import os
7
+ import posixpath
8
+ import socket
9
+ import sys
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ FRAME_DELIMITER = b"\n"
13
+ UNIX_SOCKET_BASE_DIRECTORY = "/tmp"
14
+ DAEMON_SOCKET_FILENAME = "daemon.sock"
15
+ DEFAULT_TIMEOUT_MS = 130_000
16
+
17
+
18
+ class PaneDaemonClientError(RuntimeError):
19
+ def __init__(self, message: str, code: Optional[str] = None) -> None:
20
+ super().__init__(message)
21
+ self.code = code
22
+
23
+
24
+ def resolve_pane_directory(pane_dir: Optional[str] = None) -> str:
25
+ return pane_dir or os.environ.get("PANE_DIR") or os.environ.get("FOOZOL_DIR") or os.path.join(os.path.expanduser("~"), ".pane")
26
+
27
+
28
+ def get_pane_daemon_endpoint(app_directory: str, platform: str = sys.platform) -> Dict[str, str]:
29
+ resolved_app_directory = resolve_app_directory(app_directory, platform)
30
+ if platform.startswith("win"):
31
+ return {
32
+ "transport": "pipe",
33
+ "path": get_windows_pipe_name(resolved_app_directory),
34
+ }
35
+
36
+ return {
37
+ "transport": "unix",
38
+ "path": posixpath.join(get_unix_socket_directory_name(resolved_app_directory), DAEMON_SOCKET_FILENAME),
39
+ }
40
+
41
+
42
+ def invoke_daemon(
43
+ channel: str,
44
+ args: Optional[List[Any]] = None,
45
+ pane_dir: Optional[str] = None,
46
+ timeout_ms: Optional[float] = None,
47
+ ) -> Any:
48
+ endpoint = get_pane_daemon_endpoint(resolve_pane_directory(pane_dir))
49
+ request = {
50
+ "type": "request",
51
+ "id": 1,
52
+ "channel": channel,
53
+ "args": args or [],
54
+ }
55
+ encoded = encode_frame(request)
56
+
57
+ if endpoint["transport"] == "pipe":
58
+ return invoke_windows_pipe(endpoint["path"], encoded, timeout_ms or DEFAULT_TIMEOUT_MS)
59
+ return invoke_unix_socket(endpoint["path"], encoded, timeout_ms or DEFAULT_TIMEOUT_MS)
60
+
61
+
62
+ def invoke_unix_socket(socket_path: str, encoded_request: bytes, timeout_ms: float) -> Any:
63
+ decoder = PaneDaemonFrameDecoder()
64
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
65
+ client.settimeout(timeout_ms / 1000)
66
+ try:
67
+ client.connect(socket_path)
68
+ except OSError as error:
69
+ raise PaneDaemonClientError(f"Could not connect to Pane daemon at {socket_path}: {error}") from error
70
+
71
+ client.sendall(encoded_request)
72
+ while True:
73
+ chunk = client.recv(65536)
74
+ if not chunk:
75
+ raise PaneDaemonClientError(
76
+ f"Pane daemon closed the connection before responding at {socket_path}",
77
+ "ERR_RUNPANE_DAEMON_CLOSED",
78
+ )
79
+ response = first_matching_response(decoder.push(chunk))
80
+ if response is not None:
81
+ return response
82
+
83
+
84
+ def invoke_windows_pipe(pipe_path: str, encoded_request: bytes, timeout_ms: float) -> Any:
85
+ decoder = PaneDaemonFrameDecoder()
86
+ try:
87
+ with open(pipe_path, "r+b", buffering=0) as pipe:
88
+ pipe.write(encoded_request)
89
+ while True:
90
+ chunk = pipe.read(65536)
91
+ if not chunk:
92
+ raise PaneDaemonClientError(
93
+ f"Pane daemon closed the connection before responding at {pipe_path}",
94
+ "ERR_RUNPANE_DAEMON_CLOSED",
95
+ )
96
+ response = first_matching_response(decoder.push(chunk))
97
+ if response is not None:
98
+ return response
99
+ except OSError as error:
100
+ raise PaneDaemonClientError(f"Could not connect to Pane daemon at {pipe_path}: {error}") from error
101
+
102
+
103
+ def first_matching_response(frames: List[Dict[str, Any]]) -> Optional[Any]:
104
+ for frame in frames:
105
+ if frame.get("type") != "response" or frame.get("id") != 1:
106
+ continue
107
+ if frame.get("ok") is True:
108
+ return frame.get("result")
109
+ error = frame.get("error") or {}
110
+ raise PaneDaemonClientError(error.get("message", "Pane daemon request failed"), error.get("code"))
111
+ return None
112
+
113
+
114
+ def resolve_app_directory(app_directory: str, platform: str) -> str:
115
+ if platform.startswith("win"):
116
+ return ntpath.abspath(app_directory)
117
+ return posixpath.abspath(app_directory)
118
+
119
+
120
+ def get_windows_pipe_name(app_directory: str) -> str:
121
+ digest = hashlib.sha256(app_directory.lower().encode("utf-8")).hexdigest()[:16]
122
+ return "\\\\.\\pipe\\pane-daemon-" + digest
123
+
124
+
125
+ def get_unix_socket_directory_name(app_directory: str) -> str:
126
+ digest = hashlib.sha256(app_directory.encode("utf-8")).hexdigest()[:16]
127
+ uid_suffix = f"-{os.getuid()}" if hasattr(os, "getuid") else ""
128
+ return posixpath.join(UNIX_SOCKET_BASE_DIRECTORY, f"pane-daemon{uid_suffix}-{digest}")
129
+
130
+
131
+ def encode_frame(frame: Dict[str, Any]) -> bytes:
132
+ return json.dumps(frame, separators=(",", ":")).encode("utf-8") + FRAME_DELIMITER
133
+
134
+
135
+ class PaneDaemonFrameDecoder:
136
+ def __init__(self) -> None:
137
+ self.buffer = b""
138
+
139
+ def push(self, chunk: bytes) -> List[Dict[str, Any]]:
140
+ self.buffer += chunk
141
+ frames: List[Dict[str, Any]] = []
142
+ while FRAME_DELIMITER in self.buffer:
143
+ raw_frame, self.buffer = self.buffer.split(FRAME_DELIMITER, 1)
144
+ if raw_frame.strip():
145
+ frames.append(json.loads(raw_frame.decode("utf-8")))
146
+ return frames
@@ -0,0 +1,4 @@
1
+ # Generated by scripts/generate-runpane-contract.js. Do not edit by hand.
2
+ import json
3
+
4
+ RUNPANE_CONTRACT = json.loads("{\n \"$schema\": \"./schema.json\",\n \"schemaVersion\": 1,\n \"name\": \"runpane\",\n \"description\": \"Thin installer and local control CLI for Pane\",\n \"packageInstallPolicy\": [\n \"The packages must not download, install, or configure Pane during package installation.\",\n \"Work starts only when a user runs `runpane ...`.\"\n ],\n \"compatibility\": {\n \"node\": \">=18.17.0\",\n \"python\": \">=3.8\"\n },\n \"terminology\": {\n \"repo\": \"Saved Pane repository/project record\",\n \"pane\": \"User-visible Pane session\",\n \"tool\": \"Terminal-backed tab\",\n \"agent\": \"Built-in agent command template\"\n },\n \"defaults\": {\n \"target\": \"client\",\n \"paneVersion\": \"latest\",\n \"channel\": \"stable\",\n \"format\": \"auto\",\n \"dryRun\": false,\n \"yes\": false,\n \"verbose\": false\n },\n \"enums\": {\n \"installTargets\": [\n \"client\",\n \"daemon\"\n ],\n \"artifactFormats\": [\n \"auto\",\n \"appimage\",\n \"deb\",\n \"dmg\",\n \"zip\",\n \"exe\"\n ],\n \"channels\": [\n \"stable\",\n \"nightly\"\n ],\n \"agents\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"agentTemplates\": {\n \"codex\": {\n \"title\": \"Codex\",\n \"command\": \"codex --yolo\",\n \"description\": \"Open a Codex terminal tab and allow the initial input to drive the agent.\"\n },\n \"claude\": {\n \"title\": \"Claude Code\",\n \"command\": \"claude --dangerously-skip-permissions\",\n \"description\": \"Open a Claude Code terminal tab and allow the initial input to drive the agent.\"\n }\n },\n \"commands\": [\n {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"usage\": [\n \"runpane help [command]\"\n ]\n },\n {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"usage\": [\n \"runpane setup\"\n ],\n \"interactiveEntrypoint\": true\n },\n {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"usage\": [\n \"runpane install [client|daemon] [options]\"\n ],\n \"defaultTarget\": \"client\",\n \"targets\": [\n \"client\",\n \"daemon\"\n ],\n \"unknownDaemonFlagsForwarded\": true\n },\n {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"usage\": [\n \"runpane update [options]\"\n ],\n \"target\": \"client\"\n },\n {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version and installed Pane version when detectable.\",\n \"usage\": [\n \"runpane version\",\n \"runpane --version\"\n ]\n },\n {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, and remote setup diagnostics.\",\n \"usage\": [\n \"runpane doctor\"\n ]\n },\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"usage\": [\n \"runpane agent-context [--json]\",\n \"runpane agent-context --command <command> [--json]\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"usage\": [\n \"runpane repos list [--json] [--pane-dir <path>]\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"usage\": [\n \"runpane repos add --path <path> [--name <name>] [--json] [--yes]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create one or more Pane sessions in a saved repository and open a terminal-backed tool tab.\",\n \"usage\": [\n \"runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \"runpane panes create --from-json <path|-> [--yes] [--json]\"\n ],\n \"mutates\": true,\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ]\n }\n ],\n \"flags\": {\n \"wrapper\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"description\": \"Pane release to install or inspect.\"\n },\n {\n \"name\": \"--download-dir\",\n \"value\": \"<path>\",\n \"description\": \"Directory for downloaded Pane release artifacts.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"description\": \"Use or inspect an existing Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"description\": \"Pane release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"description\": \"Print the plan without downloading or installing.\"\n },\n {\n \"name\": \"--yes\",\n \"aliases\": [\n \"-y\"\n ],\n \"description\": \"Skip interactive prompts where possible.\"\n },\n {\n \"name\": \"--verbose\",\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"remoteValue\": [\n {\n \"name\": \"--label\",\n \"value\": \"<name>\"\n },\n {\n \"name\": \"--prefer-tunnel\",\n \"value\": \"<tailscale|ssh|manual|auto>\"\n },\n {\n \"name\": \"--channel\",\n \"value\": \"<stable|nightly>\"\n },\n {\n \"name\": \"--base-url\",\n \"value\": \"<url>\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\"\n },\n {\n \"name\": \"--listen-port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--repo-ref\",\n \"value\": \"<ref>\"\n }\n ],\n \"remoteBoolean\": [\n {\n \"name\": \"--auto-listen-port\"\n },\n {\n \"name\": \"--interactive-tailscale-setup\"\n },\n {\n \"name\": \"--no-install-service\"\n },\n {\n \"name\": \"--no-tailscale-serve\"\n },\n {\n \"name\": \"--print-only\"\n }\n ],\n \"localValue\": [\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"description\": \"Connect to a Pane daemon using this Pane data directory.\"\n },\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"description\": \"Name for the registered repository or created pane/session.\"\n },\n {\n \"name\": \"--worktree-name\",\n \"value\": \"<name>\",\n \"description\": \"Worktree name to request. Defaults to --name.\"\n },\n {\n \"name\": \"--base-branch\",\n \"value\": \"<branch>\",\n \"description\": \"Base branch for the created worktree.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--title\",\n \"value\": \"<title>\",\n \"description\": \"Terminal tab title. Defaults to the selected agent title or Terminal.\"\n },\n {\n \"name\": \"--initial-input\",\n \"value\": \"<text>\",\n \"aliases\": [\n \"--prompt\"\n ],\n \"description\": \"Text to send to the terminal after the command is ready. --prompt is an alias.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"description\": \"Read a full panes.create request JSON payload from a file or stdin.\"\n },\n {\n \"name\": \"--timeout-ms\",\n \"value\": \"<milliseconds>\",\n \"description\": \"Maximum time to wait for each pane creation job.\"\n }\n ],\n \"localBoolean\": [\n {\n \"name\": \"--json\",\n \"description\": \"Print machine-readable JSON output.\"\n }\n ]\n },\n \"help\": {\n \"npm\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude>\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest\",\n \" pipx run runpane\",\n \"\",\n \"Run \\\"runpane agent-context\\\" when an agent needs Pane command context.\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z> Pane release to install\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path> Use an existing Pane executable\",\n \" --dry-run Print the plan without downloading\",\n \" --yes Skip interactive prompts where possible\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [options]\",\n \"\",\n \"Updates Pane using the same artifact selection as \\\"runpane install client\\\".\",\n \"\",\n \"Options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--pane-path <path>] [--format <format>] [--verbose]\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json Print machine-readable output\",\n \" --pane-dir <path> Connect to a specific Pane data directory\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path> Existing git repository path\",\n \" --name <name> Saved repository name; defaults to the directory name\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without adding the repo\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates Pane sessions in a saved repository and opens a terminal-backed tool tab.\",\n \"\",\n \"Options:\",\n \" --repo <selector> active, id, exact path, or saved repository name\",\n \" --name <name> Pane/session name\",\n \" --worktree-name <name> Worktree name; defaults to --name\",\n \" --base-branch <branch> Base branch for the worktree\",\n \" --agent <codex|claude> Built-in terminal template\",\n \" --tool-command <command> Custom terminal command\",\n \" --title <title> Terminal tab title\",\n \" --initial-input <text> Text sent after the command is ready\",\n \" --prompt <text> Alias for --initial-input\",\n \" --initial-input-file <path|-> Read initial input from a file or stdin\",\n \" --from-json <path|-> Read a full request payload\",\n \" --timeout-ms <milliseconds> Pane creation timeout\",\n \" --pane-dir <path> Connect to a specific Pane data directory\",\n \" --json Print machine-readable output\",\n \" --dry-run Validate and preview without creating panes\",\n \" --yes Skip confirmation for mutating commands\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command> Print the full definition for one runpane command\",\n \" --json Print machine-readable output\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ]\n },\n \"pip\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane agent-context [--json]\",\n \" runpane repos list [--json]\",\n \" runpane repos add --path <path> [--name <name>]\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude>\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" pipx run runpane install client\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \" uvx runpane@latest\",\n \"\",\n \"Run \\\"runpane agent-context\\\" when an agent needs Pane command context.\",\n \"Run \\\"runpane help panes create\\\" for pane orchestration options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [--version <latest|vX.Y.Z>] [--dry-run] [--yes]\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--pane-path <path>] [--format <format>] [--verbose]\"\n ],\n \"repos list\": [\n \"Usage:\",\n \" runpane repos list [--json] [--pane-dir <path>]\",\n \"\",\n \"Lists repositories saved in the running Pane app.\",\n \"\",\n \"Options:\",\n \" --json\",\n \" --pane-dir <path>\"\n ],\n \"repos add\": [\n \"Usage:\",\n \" runpane repos add --path <path> [--name <name>] [--json] [--yes]\",\n \"\",\n \"Registers an existing git repository with the running Pane app.\",\n \"\",\n \"Options:\",\n \" --path <path>\",\n \" --name <name>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"panes create\": [\n \"Usage:\",\n \" runpane panes create --repo <selector> --name <name> --agent <codex|claude> [options]\",\n \" runpane panes create --from-json <path|-> [--yes] [--json]\",\n \"\",\n \"Creates Pane sessions in a saved repository and opens a terminal-backed tool tab.\",\n \"\",\n \"Options:\",\n \" --repo <selector>\",\n \" --name <name>\",\n \" --worktree-name <name>\",\n \" --base-branch <branch>\",\n \" --agent <codex|claude>\",\n \" --tool-command <command>\",\n \" --title <title>\",\n \" --initial-input <text>\",\n \" --prompt <text>\",\n \" --initial-input-file <path|->\",\n \" --from-json <path|->\",\n \" --timeout-ms <milliseconds>\",\n \" --pane-dir <path>\",\n \" --json\",\n \" --dry-run\",\n \" --yes\"\n ],\n \"agent-context\": [\n \"Usage:\",\n \" runpane agent-context [--json]\",\n \" runpane agent-context --command <command> [--json]\",\n \"\",\n \"Prints Pane command context for coding agents without requiring a running Pane app.\",\n \"\",\n \"Options:\",\n \" --command <command>\",\n \" --json\",\n \"\",\n \"Examples:\",\n \" runpane agent-context\",\n \" runpane agent-context --json\",\n \" runpane agent-context --command \\\"panes create\\\"\",\n \" runpane agent-context --command \\\"panes create\\\" --json\"\n ]\n }\n },\n \"docs\": {\n \"maintainerRules\": [\n \"Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\",\n \"Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\",\n \"The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\",\n \"Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\",\n \"Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\",\n \"`pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\",\n \"Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\"\n ],\n \"recommendedQuickStarts\": [\n \"npx --yes runpane@latest\",\n \"npm i -g runpane && runpane setup\",\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"npmCommands\": [\n \"npx --yes runpane@latest\",\n \"npx --yes runpane@latest setup\",\n \"npx --yes runpane@latest install client\",\n \"npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \"pnpm dlx runpane@latest\",\n \"pnpm dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"npm i -g runpane && runpane\",\n \"npm i -g runpane && runpane setup\",\n \"npm i -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"pnpm add -g runpane && runpane\",\n \"pnpm add -g runpane && runpane setup\",\n \"pnpm add -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"yarn dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"bunx runpane@latest install daemon --label \\\"My Server\\\"\"\n ],\n \"pythonCommands\": [\n \"pipx run runpane\",\n \"pipx run runpane setup\",\n \"python -m pip install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"python -m runpane setup\",\n \"runpane install daemon --label \\\"My Server\\\"\",\n \"pipx install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"uvx runpane@latest\",\n \"uvx runpane@latest setup\",\n \"uvx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"python -m runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"packageManagerNotes\": [\n \"Use `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\",\n \"Do not document `pnpm install runpane` as the public CLI install path.\"\n ],\n \"commandUsages\": [\n \"runpane\",\n \"runpane setup\",\n \"runpane install\",\n \"runpane install client\",\n \"runpane install daemon\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\",\n \"runpane repos list --json\",\n \"runpane repos add --path /path/to/repo --name Pane --yes --json\",\n \"runpane panes create --repo active --name issue-252 --agent codex --prompt \\\"Kick off the discussion skill for issue 252\\\" --yes\",\n \"runpane panes create --from-json panes.json --yes --json\",\n \"runpane help\",\n \"runpane <command> --help\"\n ],\n \"commandDescriptions\": [\n \"`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. In non-interactive shells or CI, both forms must print help and exit successfully instead of waiting for input.\",\n \"`runpane install` is an alias for `runpane install client`.\",\n \"`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\",\n \"`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\",\n \"The wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\",\n \"`runpane update` uses the same release resolution and installer path as `install client`.\",\n \"`runpane version` prints the wrapper package version, the installed Pane version when detectable, and the latest GitHub release version when reachable.\",\n \"`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, and remote-daemon hints.\",\n \"`runpane agent-context` prints a brief, token-efficient command schema for coding agents without connecting to the Pane daemon.\",\n \"`runpane agent-context --command \\\"panes create\\\"` prints the detailed definition for one command. Add `--json` for machine-readable output.\",\n \"`runpane repos list` connects to the running local Pane daemon and prints saved repository records.\",\n \"`runpane repos add` registers an existing git repository with the running local Pane daemon. It does not create directories or initialize git repositories by default.\",\n \"`runpane panes create` connects to the running local Pane daemon, resolves the requested repository, creates Pane sessions, opens terminal-backed tool tabs, and optionally sends initial input to the started tool.\",\n \"`runpane panes create --prompt` is an alias for `--initial-input`; request JSON and daemon payloads should use the canonical `initialInput` field.\"\n ],\n \"wrapperFlagNote\": \"The top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\",\n \"localControlFlagNote\": \"`runpane repos list` and `runpane panes create` use the local framed daemon socket/pipe for a running Pane app. `--pane-dir` points the wrapper at a non-default Pane data directory, such as `PANE_DIR=~/.pane_test` in development. `runpane agent-context` is local/offline and can be used before Pane is running.\",\n \"daemonFlagNote\": \"Unknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\",\n \"downloadAttribution\": [\n \"The npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\",\n \"The PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\",\n \"Wrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\",\n \"If the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\"\n ],\n \"publishingCredentials\": [\n \"Local implementation, build, and dry-run validation do not need npm or PyPI API tokens.\",\n \"Release publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\",\n \"Fallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\"\n ]\n },\n \"testFixtures\": {\n \"parserSamples\": [\n [\n \"setup\"\n ],\n [\n \"install\"\n ],\n [\n \"install\",\n \"client\",\n \"--version\",\n \"v2.2.8\",\n \"--format\",\n \"dmg\",\n \"--download-dir\",\n \"/tmp/pane-downloads\",\n \"--dry-run\",\n \"--yes\"\n ],\n [\n \"install\",\n \"daemon\",\n \"--label\",\n \"VM\",\n \"--prefer-tunnel\",\n \"ssh\",\n \"--channel\",\n \"nightly\",\n \"--base-url\",\n \"https://example.test\",\n \"--pane-dir\",\n \"/tmp/pane\",\n \"--listen-port\",\n \"4555\",\n \"--auto-listen-port\",\n \"--print-only\",\n \"--repo-ref\",\n \"main\",\n \"--unknown-future-flag\",\n \"future-value\",\n \"--dry-run\",\n \"--verbose\"\n ],\n [\n \"update\",\n \"--version\",\n \"latest\",\n \"--format\",\n \"appimage\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--dry-run\"\n ],\n [\n \"doctor\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--format\",\n \"zip\",\n \"--verbose\"\n ],\n [\n \"agent-context\"\n ],\n [\n \"agent-context\",\n \"--command\",\n \"panes create\",\n \"--json\"\n ],\n [\n \"repos\",\n \"list\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"repos\",\n \"add\",\n \"--path\",\n \"/tmp/repo\",\n \"--name\",\n \"Repo\",\n \"--dry-run\",\n \"--yes\",\n \"--json\",\n \"--pane-dir\",\n \"/tmp/pane\"\n ],\n [\n \"panes\",\n \"create\",\n \"--repo\",\n \"active\",\n \"--name\",\n \"issue-252\",\n \"--agent\",\n \"codex\",\n \"--prompt\",\n \"Kick off discussion\",\n \"--dry-run\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"panes\",\n \"create\",\n \"--from-json\",\n \"-\",\n \"--yes\",\n \"--json\"\n ],\n [\n \"--version\"\n ]\n ],\n \"topLevelHelpIncludes\": [\n \"runpane setup\",\n \"runpane install\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane agent-context\",\n \"runpane repos list\",\n \"runpane repos add\",\n \"runpane panes create\"\n ],\n \"npmHelpIncludes\": [\n \"pnpm dlx runpane@latest\",\n \"npx --yes runpane@latest\"\n ],\n \"pipHelpIncludes\": [\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"installHelpIncludes\": [\n \"--version <latest|vX.Y.Z>\",\n \"--format <auto|appimage|deb|dmg|zip|exe>\",\n \"--download-dir <path>\",\n \"--pane-path <path>\",\n \"--label <name>\",\n \"--prefer-tunnel <tailscale|ssh|manual|auto>\",\n \"--repo-ref <ref>\"\n ]\n },\n \"jsonSchemas\": {\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"repoListResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repos\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"repos\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"id\",\n \"name\",\n \"path\",\n \"active\",\n \"sessionCount\"\n ],\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"sessionCount\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"path\"\n ],\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"repoAddResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"created\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"created\": {\n \"type\": \"boolean\"\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"preview\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"path\",\n \"alreadyExists\",\n \"wouldCreate\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"alreadyExists\": {\n \"type\": \"boolean\"\n },\n \"wouldCreate\": {\n \"type\": \"boolean\"\n },\n \"environment\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateRequest\": {\n \"type\": \"object\",\n \"required\": [\n \"repo\",\n \"panes\"\n ],\n \"properties\": {\n \"repo\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"active\": {\n \"const\": true\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"panes\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"tool\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"worktreeName\": {\n \"type\": \"string\"\n },\n \"baseBranch\": {\n \"type\": \"string\"\n },\n \"sessionPrompt\": {\n \"type\": \"string\"\n },\n \"tool\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"agent\"\n ],\n \"properties\": {\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"command\"\n ],\n \"properties\": {\n \"command\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"initialInput\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"dryRun\": {\n \"type\": \"boolean\"\n },\n \"timeoutMs\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n },\n \"paneCreateResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"repo\",\n \"items\"\n ],\n \"properties\": {\n \"ok\": {\n \"type\": \"boolean\"\n },\n \"repo\": {\n \"$ref\": \"#/jsonSchemas/repoListResult/properties/repos/items\"\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"name\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"sessionId\": {\n \"type\": \"string\"\n },\n \"paneId\": {\n \"type\": \"string\"\n },\n \"panelId\": {\n \"type\": \"string\"\n },\n \"worktreePath\": {\n \"type\": \"string\"\n },\n \"tool\": {\n \"type\": \"object\",\n \"required\": [\n \"title\",\n \"command\"\n ],\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"agent\": {\n \"enum\": [\n \"codex\",\n \"claude\"\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"index\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"index\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextBriefResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"summary\",\n \"rules\",\n \"tools\",\n \"detailCommand\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"brief\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"rules\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"tools\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"arguments\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"detailCommand\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"agentContextCommandResult\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"mode\",\n \"source\",\n \"command\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": true\n },\n \"mode\": {\n \"const\": \"command\"\n },\n \"source\": {\n \"const\": \"runpane-contract\"\n },\n \"command\": {\n \"type\": \"object\",\n \"required\": [\n \"name\",\n \"summary\",\n \"details\",\n \"arguments\",\n \"examples\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"summary\": {\n \"type\": \"string\"\n },\n \"details\": {\n \"type\": \"string\"\n },\n \"requiresPaneDaemon\": {\n \"type\": \"boolean\"\n },\n \"mutates\": {\n \"type\": \"boolean\"\n },\n \"arguments\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"examples\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"jsonSchemas\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"notes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n },\n \"agentContext\": {\n \"brief\": {\n \"title\": \"Pane agent context\",\n \"summary\": \"Pane lets a developer manage repositories and user-visible panes. Agents can use runpane to list/add Pane repositories and create panes with terminal-backed tools.\",\n \"rules\": [\n \"Start with `runpane repos list --json` to find the saved repository when unsure.\",\n \"If the repository exists on disk but is not saved in Pane, use `runpane repos add --path <repo> --yes --json` before creating panes.\",\n \"Use `runpane panes create` to create a Pane session and open a built-in agent or custom terminal command.\",\n \"Use `runpane agent-context --command <command>` for detailed command definitions only when needed.\"\n ],\n \"detailCommand\": \"runpane agent-context --command <command> [--json]\",\n \"tools\": [\n {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"arguments\": [\n \"--command <command>\",\n \"--json\"\n ]\n },\n {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"arguments\": [\n \"--json\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"arguments\": [\n \"--path <path>\",\n \"--name <name>\",\n \"--yes\",\n \"--json\",\n \"--dry-run\",\n \"--pane-dir <path>\"\n ]\n },\n {\n \"name\": \"panes create\",\n \"summary\": \"Create one or more Pane sessions in a saved repository and open a terminal-backed tool tab.\",\n \"arguments\": [\n \"--repo <selector>\",\n \"--name <name>\",\n \"--agent <codex|claude>\",\n \"--tool-command <command>\",\n \"--prompt <text>\",\n \"--from-json <path|->\",\n \"--yes\",\n \"--json\"\n ]\n }\n ]\n },\n \"commands\": {\n \"help\": {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"details\": \"Use this when you need human-readable usage text for a command.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Optional command topic such as \\\"panes create\\\".\"\n }\n ],\n \"examples\": [\n \"runpane help\",\n \"runpane help panes create\"\n ],\n \"notes\": [\n \"Help text is generated from the runpane contract.\"\n ]\n },\n \"setup\": {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"details\": \"Use this for a human-driven Pane setup flow, not for unattended agent orchestration.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [],\n \"examples\": [\n \"runpane setup\"\n ],\n \"notes\": [\n \"In non-interactive shells, setup prints help and exits successfully.\"\n ]\n },\n \"install\": {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"details\": \"Installs or launches Pane release artifacts at command runtime. `install daemon` forwards remote setup flags to Pane.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"target\",\n \"value\": \"<client|daemon>\",\n \"required\": false,\n \"description\": \"Install the desktop client or configure a remote daemon host.\"\n },\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to install.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip interactive prompts where possible.\"\n }\n ],\n \"examples\": [\n \"runpane install client\",\n \"runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"notes\": [\n \"Package install itself is inert; work begins only when runpane is executed.\"\n ]\n },\n \"update\": {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"details\": \"Resolves and installs the selected Pane client release.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"required\": false,\n \"description\": \"Pane release to update to.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"required\": false,\n \"description\": \"Release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Print the update plan without downloading.\"\n }\n ],\n \"examples\": [\n \"runpane update\",\n \"runpane update --version latest\"\n ],\n \"notes\": [\n \"Equivalent artifact selection to `runpane install client`.\"\n ]\n },\n \"version\": {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version and installed Pane version when detectable.\",\n \"details\": \"Use this for diagnostics or to confirm which wrapper is available.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Inspect a specific Pane executable.\"\n }\n ],\n \"examples\": [\n \"runpane version\",\n \"runpane --version\"\n ],\n \"notes\": []\n },\n \"doctor\": {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, and remote setup diagnostics.\",\n \"details\": \"Use this when runpane install/update behavior needs environment diagnostics.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Use an existing Pane executable for diagnostics.\"\n },\n {\n \"name\": \"--verbose\",\n \"required\": false,\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"examples\": [\n \"runpane doctor\",\n \"runpane doctor --verbose\"\n ],\n \"notes\": []\n },\n \"agent-context\": {\n \"name\": \"agent-context\",\n \"summary\": \"Print token-efficient Pane command context for coding agents.\",\n \"details\": \"Use this first when an agent needs to discover Pane primitives. It is local/offline and does not require a running Pane app.\",\n \"requiresPaneDaemon\": false,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Print the detailed definition for one command, for example \\\"panes create\\\".\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane agent-context\",\n \"runpane agent-context --command \\\"panes create\\\" --json\"\n ],\n \"jsonSchemas\": [\n \"agentContextBriefResult\",\n \"agentContextCommandResult\"\n ],\n \"notes\": [\n \"Default output is brief so AGENTS.md can point here without bloating context.\"\n ]\n },\n \"repos list\": {\n \"name\": \"repos list\",\n \"summary\": \"List repositories saved in the running Pane app.\",\n \"details\": \"Use this to find the right Pane-managed repository before creating panes. If the repo is missing, use `repos add` first.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": false,\n \"arguments\": [\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable repository records.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos list --json\"\n ],\n \"jsonSchemas\": [\n \"repoListResult\"\n ],\n \"notes\": [\n \"Requires a running Pane app or daemon for the selected Pane data directory.\"\n ]\n },\n \"repos add\": {\n \"name\": \"repos add\",\n \"summary\": \"Register an existing git repository with the running Pane app.\",\n \"details\": \"Use this when the repository exists on disk but is not saved in Pane yet. It validates the path as a git repository.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--path\",\n \"value\": \"<path>\",\n \"required\": true,\n \"description\": \"Existing git repository path to register with Pane.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": false,\n \"description\": \"Saved repository name; defaults to the directory name.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n },\n {\n \"name\": \"--dry-run\",\n \"required\": false,\n \"description\": \"Validate and preview without adding the repo.\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\",\n \"required\": false,\n \"description\": \"Connect to a specific Pane data directory.\"\n }\n ],\n \"examples\": [\n \"runpane repos add --path /path/to/repo --yes --json\"\n ],\n \"jsonSchemas\": [\n \"repoAddRequest\",\n \"repoAddResult\"\n ],\n \"notes\": [\n \"It does not create directories or initialize git repositories by default.\"\n ]\n },\n \"panes create\": {\n \"name\": \"panes create\",\n \"summary\": \"Create one or more Pane sessions in a saved repository and open a terminal-backed tool tab.\",\n \"details\": \"Use this to set up user-visible panes for work. Select a saved repository, choose a built-in agent or custom terminal command, and optionally send initial input after the tool starts.\",\n \"requiresPaneDaemon\": true,\n \"mutates\": true,\n \"arguments\": [\n {\n \"name\": \"--repo\",\n \"value\": \"<selector>\",\n \"required\": true,\n \"description\": \"Repository selector: active, id, exact path, or saved repository name.\"\n },\n {\n \"name\": \"--name\",\n \"value\": \"<name>\",\n \"required\": true,\n \"description\": \"Pane/session name.\"\n },\n {\n \"name\": \"--agent\",\n \"value\": \"<codex|claude>\",\n \"required\": false,\n \"description\": \"Built-in agent terminal template to open.\"\n },\n {\n \"name\": \"--tool-command\",\n \"value\": \"<command>\",\n \"required\": false,\n \"description\": \"Custom terminal command to run instead of a built-in agent.\"\n },\n {\n \"name\": \"--prompt\",\n \"value\": \"<text>\",\n \"required\": false,\n \"description\": \"Alias for --initial-input; sends text after the command is ready.\"\n },\n {\n \"name\": \"--initial-input-file\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read initial input from a file or stdin.\"\n },\n {\n \"name\": \"--from-json\",\n \"value\": \"<path|->\",\n \"required\": false,\n \"description\": \"Read a full panes.create request JSON payload.\"\n },\n {\n \"name\": \"--yes\",\n \"required\": false,\n \"description\": \"Skip confirmation for mutating commands.\"\n },\n {\n \"name\": \"--json\",\n \"required\": false,\n \"description\": \"Print machine-readable output.\"\n }\n ],\n \"examples\": [\n \"runpane panes create --repo active --name issue-257 --agent codex --prompt \\\"Plan this issue\\\" --yes\",\n \"runpane panes create --from-json panes.json --yes --json\"\n ],\n \"jsonSchemas\": [\n \"paneCreateRequest\",\n \"paneCreateResult\"\n ],\n \"notes\": [\n \"At least one of --agent or --tool-command is required unless --from-json is used.\",\n \"The built-in agent templates come from the runpane contract; custom terminal commands can pass agent-specific flags when requested by the user.\"\n ]\n }\n },\n \"managedBlock\": [\n \"## Pane\",\n \"\",\n \"The developer is using Pane for this repository. Pane can manage saved repositories and create user-visible panes with terminal-backed tools for planning, discussion, and implementation work.\",\n \"\",\n \"Use `runpane agent-context` for a brief Pane command schema. Use `runpane agent-context --command \\\"panes create\\\"` or another command name for the detailed schema only when needed.\",\n \"\",\n \"Common commands:\",\n \"- `runpane repos list --json`\",\n \"- `runpane repos add --path <repo> --yes --json`\",\n \"- `runpane panes create --repo active --name <name> --agent codex --prompt \\\"<task>\\\" --yes`\"\n ]\n }\n}")
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from typing import Any, Dict, Optional
7
+
8
+ from .daemon_client import invoke_daemon
9
+ from .generated_contract import RUNPANE_CONTRACT
10
+
11
+
12
+ def run_repos_list(parsed: Any) -> int:
13
+ result = invoke_daemon("runpane:repos:list", pane_dir=parsed.pane_dir)
14
+
15
+ if parsed.json:
16
+ print_json(result)
17
+ return 0
18
+
19
+ repos = result.get("repos", [])
20
+ if not repos:
21
+ print("No Pane repositories found.")
22
+ return 0
23
+
24
+ for repo in repos:
25
+ marker = "*" if repo.get("active") else " "
26
+ environment = f" {repo.get('environment')}" if repo.get("environment") else ""
27
+ print(f"{marker} {repo.get('id')}\t{repo.get('name')}\t{repo.get('path')}\t{repo.get('sessionCount')} sessions{environment}")
28
+ return 0
29
+
30
+
31
+ def run_repos_add(parsed: Any) -> int:
32
+ request = build_repo_add_request(parsed)
33
+ confirm_repo_add(parsed, request)
34
+ result = invoke_daemon("runpane:repos:add", [request], pane_dir=parsed.pane_dir)
35
+
36
+ if parsed.json:
37
+ print_json(result)
38
+ else:
39
+ print_repo_add_result(result)
40
+
41
+ return 0
42
+
43
+
44
+ def run_panes_create(parsed: Any) -> int:
45
+ request = build_pane_create_request(parsed)
46
+ confirm_pane_create(parsed, request)
47
+ result = invoke_daemon(
48
+ "runpane:panes:create",
49
+ [request],
50
+ pane_dir=parsed.pane_dir,
51
+ timeout_ms=(parsed.timeout_ms or 120_000) + 10_000,
52
+ )
53
+
54
+ if parsed.json:
55
+ print_json(result)
56
+ else:
57
+ print_pane_create_result(result)
58
+
59
+ return 0 if result.get("ok") else 1
60
+
61
+
62
+ def build_repo_add_request(parsed: Any) -> Dict[str, Any]:
63
+ if not parsed.repo_path:
64
+ raise ValueError("runpane repos add requires --path.")
65
+
66
+ return {
67
+ "path": parsed.repo_path,
68
+ **optional_value("name", parsed.name),
69
+ **optional_value("dryRun", True if parsed.dry_run else None),
70
+ }
71
+
72
+
73
+ def build_pane_create_request(parsed: Any) -> Dict[str, Any]:
74
+ if parsed.from_json:
75
+ payload = json.loads(read_input_source(parsed.from_json))
76
+ if not isinstance(payload, dict):
77
+ raise ValueError("--from-json payload must be an object.")
78
+ if parsed.dry_run:
79
+ payload["dryRun"] = True
80
+ if parsed.timeout_ms is not None:
81
+ payload["timeoutMs"] = parsed.timeout_ms
82
+ return payload
83
+
84
+ if not parsed.repo:
85
+ raise ValueError("runpane panes create requires --repo unless --from-json is used.")
86
+ if not parsed.name:
87
+ raise ValueError("runpane panes create requires --name unless --from-json is used.")
88
+
89
+ return {
90
+ "repo": parsed.repo,
91
+ "panes": [{
92
+ "name": parsed.name,
93
+ **optional_value("worktreeName", parsed.worktree_name),
94
+ **optional_value("baseBranch", parsed.base_branch),
95
+ "tool": build_tool_spec(parsed),
96
+ }],
97
+ **optional_value("dryRun", True if parsed.dry_run else None),
98
+ **optional_value("timeoutMs", parsed.timeout_ms),
99
+ }
100
+
101
+
102
+ def build_tool_spec(parsed: Any) -> Dict[str, Any]:
103
+ if parsed.agent and parsed.tool_command:
104
+ raise ValueError("Use either --agent or --tool-command, not both.")
105
+
106
+ initial_input = resolve_initial_input(parsed)
107
+ agent = parsed.agent
108
+
109
+ if not agent and not parsed.tool_command:
110
+ if not is_interactive_shell():
111
+ raise ValueError("runpane panes create requires --agent or --tool-command in non-interactive shells.")
112
+ agent = ask_agent_choice()
113
+
114
+ if agent:
115
+ return {
116
+ "agent": agent,
117
+ **optional_value("title", parsed.title),
118
+ **optional_value("initialInput", initial_input),
119
+ }
120
+
121
+ if not parsed.tool_command:
122
+ raise ValueError("runpane panes create requires --agent or --tool-command.")
123
+
124
+ return {
125
+ "command": parsed.tool_command,
126
+ **optional_value("title", parsed.title),
127
+ **optional_value("initialInput", initial_input),
128
+ }
129
+
130
+
131
+ def resolve_initial_input(parsed: Any) -> Optional[str]:
132
+ if parsed.initial_input and parsed.initial_input_file:
133
+ raise ValueError("Use either --initial-input/--prompt or --initial-input-file, not both.")
134
+ if parsed.initial_input_file:
135
+ return read_input_source(parsed.initial_input_file)
136
+ return parsed.initial_input
137
+
138
+
139
+ def confirm_repo_add(parsed: Any, request: Dict[str, Any]) -> None:
140
+ if parsed.dry_run or parsed.yes:
141
+ return
142
+ if not is_interactive_shell():
143
+ raise ValueError("runpane repos add mutates Pane state. Rerun with --yes in non-interactive shells.")
144
+
145
+ label = f"{request.get('name')} at {request.get('path')}" if request.get("name") else request.get("path")
146
+ answer = input(f"Add Pane repo {label}? [y/N] ").strip().lower()
147
+ if answer not in {"y", "yes"}:
148
+ raise ValueError("Cancelled.")
149
+
150
+
151
+ def confirm_pane_create(parsed: Any, request: Dict[str, Any]) -> None:
152
+ if parsed.dry_run or parsed.yes:
153
+ return
154
+ if not is_interactive_shell():
155
+ raise ValueError("runpane panes create mutates Pane state. Rerun with --yes in non-interactive shells.")
156
+
157
+ count = len(request.get("panes", []))
158
+ answer = input(f"Create {count} Pane pane{'s' if count != 1 else ''}? [y/N] ").strip().lower()
159
+ if answer not in {"y", "yes"}:
160
+ raise ValueError("Cancelled.")
161
+
162
+
163
+ def ask_agent_choice() -> str:
164
+ agents = RUNPANE_CONTRACT["enums"]["agents"]
165
+ print("Choose an agent:")
166
+ for index, agent in enumerate(agents, start=1):
167
+ print(f"{index}) {RUNPANE_CONTRACT['agentTemplates'][agent]['title']}")
168
+
169
+ while True:
170
+ answer = input("Agent [1]: ").strip().lower()
171
+ if not answer:
172
+ return agents[0]
173
+ if answer.isdigit() and 1 <= int(answer) <= len(agents):
174
+ return agents[int(answer) - 1]
175
+ if answer in agents:
176
+ return answer
177
+ print(f"Choose one of: {', '.join(agents)}")
178
+
179
+
180
+ def read_input_source(source: str) -> str:
181
+ if source == "-":
182
+ return sys.stdin.read()
183
+ with open(source, "r", encoding="utf-8") as handle:
184
+ return handle.read()
185
+
186
+
187
+ def print_json(value: Any) -> None:
188
+ print(json.dumps(value, indent=2))
189
+
190
+
191
+ def print_repo_add_result(result: Dict[str, Any]) -> None:
192
+ preview = result.get("preview") or {}
193
+ if result.get("dryRun") and preview:
194
+ if preview.get("alreadyExists"):
195
+ print(f"Repo already exists: {preview.get('name')}\t{preview.get('path')}")
196
+ return
197
+ print(f"Would add Pane repo {preview.get('name')}\t{preview.get('path')}")
198
+ return
199
+
200
+ repo = result.get("repo")
201
+ if repo:
202
+ action = "Added Pane repo" if result.get("created") else "Repo already exists"
203
+ print(f"{action}: {repo.get('id')}\t{repo.get('name')}\t{repo.get('path')}")
204
+ return
205
+
206
+ print("Repo add completed.")
207
+
208
+
209
+ def print_pane_create_result(result: Dict[str, Any]) -> None:
210
+ for item in result.get("items", []):
211
+ name = item.get("name") or f"pane {item.get('index')}"
212
+ if item.get("ok"):
213
+ worktree = f" at {item.get('worktreePath')}" if item.get("worktreePath") else ""
214
+ print(f"Created {name}: session {item.get('sessionId', 'unknown')} panel {item.get('panelId', 'unknown')}{worktree}")
215
+ else:
216
+ error = item.get("error") or {}
217
+ print(f"Failed {name}: {error.get('message', 'unknown error')}", file=sys.stderr)
218
+
219
+
220
+ def optional_value(key: str, value: Any) -> Dict[str, Any]:
221
+ return {key: value} if value is not None else {}
222
+
223
+
224
+ def is_interactive_shell() -> bool:
225
+ return bool(sys.stdin.isatty() and sys.stdout.isatty() and not os.environ.get("CI"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runpane
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Thin PyPI installer and remote setup CLI for Pane
5
5
  Author-email: Dcouple Inc <hello@dcouple.ai>
6
6
  License: AGPL-3.0
@@ -2,11 +2,14 @@ README.md
2
2
  pyproject.toml
3
3
  src/runpane/__init__.py
4
4
  src/runpane/__main__.py
5
+ src/runpane/agent_context.py
5
6
  src/runpane/cli.py
7
+ src/runpane/daemon_client.py
6
8
  src/runpane/doctor.py
7
9
  src/runpane/download.py
8
10
  src/runpane/generated_contract.py
9
11
  src/runpane/installers.py
12
+ src/runpane/local_control.py
10
13
  src/runpane/platforms.py
11
14
  src/runpane/releases.py
12
15
  src/runpane/version.py
@@ -1 +0,0 @@
1
- __version__ = "2.3.0"
@@ -1,4 +0,0 @@
1
- # Generated by scripts/generate-runpane-contract.js. Do not edit by hand.
2
- import json
3
-
4
- RUNPANE_CONTRACT = json.loads("{\n \"$schema\": \"./schema.json\",\n \"schemaVersion\": 1,\n \"name\": \"runpane\",\n \"description\": \"Thin installer and local control CLI for Pane\",\n \"packageInstallPolicy\": [\n \"The packages must not download, install, or configure Pane during package installation.\",\n \"Work starts only when a user runs `runpane ...`.\"\n ],\n \"compatibility\": {\n \"node\": \">=18.17.0\",\n \"python\": \">=3.8\"\n },\n \"terminology\": {\n \"repo\": \"Saved Pane repository/project record\",\n \"pane\": \"User-visible Pane session\",\n \"tool\": \"Terminal-backed tab\",\n \"agent\": \"Built-in agent command template\"\n },\n \"defaults\": {\n \"target\": \"client\",\n \"paneVersion\": \"latest\",\n \"channel\": \"stable\",\n \"format\": \"auto\",\n \"dryRun\": false,\n \"yes\": false,\n \"verbose\": false\n },\n \"enums\": {\n \"installTargets\": [\n \"client\",\n \"daemon\"\n ],\n \"artifactFormats\": [\n \"auto\",\n \"appimage\",\n \"deb\",\n \"dmg\",\n \"zip\",\n \"exe\"\n ],\n \"channels\": [\n \"stable\",\n \"nightly\"\n ]\n },\n \"commands\": [\n {\n \"name\": \"help\",\n \"summary\": \"Show help for runpane or a specific command.\",\n \"usage\": [\n \"runpane help [command]\"\n ]\n },\n {\n \"name\": \"setup\",\n \"summary\": \"Open the guided setup wizard for install, remote host setup, update, and diagnostics.\",\n \"usage\": [\n \"runpane setup\"\n ],\n \"interactiveEntrypoint\": true\n },\n {\n \"name\": \"install\",\n \"summary\": \"Install Pane on this machine or configure this machine as a remote daemon host.\",\n \"usage\": [\n \"runpane install [client|daemon] [options]\"\n ],\n \"defaultTarget\": \"client\",\n \"targets\": [\n \"client\",\n \"daemon\"\n ],\n \"unknownDaemonFlagsForwarded\": true\n },\n {\n \"name\": \"update\",\n \"summary\": \"Update the Pane desktop app using the same artifact path as install client.\",\n \"usage\": [\n \"runpane update [options]\"\n ],\n \"target\": \"client\"\n },\n {\n \"name\": \"version\",\n \"summary\": \"Print the runpane wrapper version and installed Pane version when detectable.\",\n \"usage\": [\n \"runpane version\",\n \"runpane --version\"\n ]\n },\n {\n \"name\": \"doctor\",\n \"summary\": \"Run platform, release, installed Pane, and remote setup diagnostics.\",\n \"usage\": [\n \"runpane doctor\"\n ]\n }\n ],\n \"flags\": {\n \"wrapper\": [\n {\n \"name\": \"--version\",\n \"value\": \"<latest|vX.Y.Z>\",\n \"description\": \"Pane release to install or inspect.\"\n },\n {\n \"name\": \"--download-dir\",\n \"value\": \"<path>\",\n \"description\": \"Directory for downloaded Pane release artifacts.\"\n },\n {\n \"name\": \"--pane-path\",\n \"value\": \"<path>\",\n \"description\": \"Use or inspect an existing Pane executable.\"\n },\n {\n \"name\": \"--format\",\n \"value\": \"<auto|appimage|deb|dmg|zip|exe>\",\n \"description\": \"Pane release artifact format.\"\n },\n {\n \"name\": \"--dry-run\",\n \"description\": \"Print the plan without downloading or installing.\"\n },\n {\n \"name\": \"--yes\",\n \"aliases\": [\n \"-y\"\n ],\n \"description\": \"Skip interactive prompts where possible.\"\n },\n {\n \"name\": \"--verbose\",\n \"description\": \"Print extra diagnostics.\"\n }\n ],\n \"remoteValue\": [\n {\n \"name\": \"--label\",\n \"value\": \"<name>\"\n },\n {\n \"name\": \"--prefer-tunnel\",\n \"value\": \"<tailscale|ssh|manual|auto>\"\n },\n {\n \"name\": \"--channel\",\n \"value\": \"<stable|nightly>\"\n },\n {\n \"name\": \"--base-url\",\n \"value\": \"<url>\"\n },\n {\n \"name\": \"--pane-dir\",\n \"value\": \"<path>\"\n },\n {\n \"name\": \"--listen-port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--port\",\n \"value\": \"<port>\"\n },\n {\n \"name\": \"--repo-ref\",\n \"value\": \"<ref>\"\n }\n ],\n \"remoteBoolean\": [\n {\n \"name\": \"--auto-listen-port\"\n },\n {\n \"name\": \"--interactive-tailscale-setup\"\n },\n {\n \"name\": \"--no-install-service\"\n },\n {\n \"name\": \"--no-tailscale-serve\"\n },\n {\n \"name\": \"--print-only\"\n }\n ]\n },\n \"help\": {\n \"npm\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest\",\n \" pipx run runpane\",\n \"\",\n \"Run \\\"runpane help install\\\" for install options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install client\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z> Pane release to install\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path> Use an existing Pane executable\",\n \" --dry-run Print the plan without downloading\",\n \" --yes Skip interactive prompts where possible\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" npx --yes runpane@latest\",\n \" npm i -g runpane && runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [options]\",\n \"\",\n \"Updates Pane using the same artifact selection as \\\"runpane install client\\\".\",\n \"\",\n \"Options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--pane-path <path>] [--format <format>] [--verbose]\"\n ]\n },\n \"pip\": {\n \"default\": [\n \"Usage:\",\n \" runpane\",\n \" runpane setup\",\n \" runpane install [client|daemon] [options]\",\n \" runpane update [options]\",\n \" runpane version\",\n \" runpane doctor\",\n \" runpane help [command]\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\",\n \"\",\n \"Advanced examples:\",\n \" pipx run runpane install client\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \" uvx runpane@latest\",\n \"\",\n \"Run \\\"runpane help install\\\" for install options.\"\n ],\n \"install\": [\n \"Usage:\",\n \" runpane install [client|daemon] [options]\",\n \"\",\n \"Examples:\",\n \" npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \" pnpm dlx runpane@latest install daemon --prefer-tunnel ssh --label \\\"VM\\\"\",\n \" pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"\",\n \"Wrapper options:\",\n \" --version <latest|vX.Y.Z>\",\n \" --format <auto|appimage|deb|dmg|zip|exe>\",\n \" --download-dir <path>\",\n \" --pane-path <path>\",\n \" --dry-run\",\n \" --yes\",\n \" --verbose\",\n \"\",\n \"Daemon passthrough options:\",\n \" --label <name>\",\n \" --prefer-tunnel <tailscale|ssh|manual|auto>\",\n \" --channel <stable|nightly>\",\n \" --base-url <url>\",\n \" --pane-dir <path>\",\n \" --listen-port <port> / --port <port>\",\n \" --auto-listen-port\",\n \" --interactive-tailscale-setup\",\n \" --no-install-service\",\n \" --no-tailscale-serve\",\n \" --print-only\",\n \" --repo-ref <ref>\"\n ],\n \"setup\": [\n \"Usage:\",\n \" runpane setup\",\n \"\",\n \"Opens the guided setup for desktop install, remote host setup, update, and diagnostics.\",\n \"\",\n \"Quick start:\",\n \" pipx run runpane\",\n \" python -m pip install runpane && python -m runpane setup\"\n ],\n \"update\": [\n \"Usage:\",\n \" runpane update [--version <latest|vX.Y.Z>] [--dry-run] [--yes]\"\n ],\n \"version\": [\n \"Usage:\",\n \" runpane version\",\n \" runpane --version\"\n ],\n \"doctor\": [\n \"Usage:\",\n \" runpane doctor [--pane-path <path>] [--format <format>] [--verbose]\"\n ]\n }\n },\n \"docs\": {\n \"maintainerRules\": [\n \"Treat `contracts/runpane/contract.json` as the source of truth for both wrapper packages and generated docs.\",\n \"Every command, flag, platform default, artifact-selection rule, and attribution rule change must be reflected in the contract.\",\n \"The npm and PyPI wrappers must expose the same command behavior unless the contract explicitly documents a package-manager-specific difference.\",\n \"Root `README.md` and package READMEs should lead with one guided quick-start command. Explicit commands, package-manager variants, and flags belong in an Advanced section.\",\n \"Release version bumps must keep root `package.json`, `packages/runpane`, and `packages/runpane-py` versions in sync. Run `pnpm run check:runpane-package-versions` before release.\",\n \"`pnpm run test:runpane-contract` must pass before changing wrapper command parsing, help output, platform defaults, release asset selection, or generated contract artifacts.\",\n \"Token-based npm or PyPI publishing is a temporary fallback. Prefer trusted publishing once the package names are reserved and trusted publishers are configured.\"\n ],\n \"recommendedQuickStarts\": [\n \"npx --yes runpane@latest\",\n \"npm i -g runpane && runpane setup\",\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"npmCommands\": [\n \"npx --yes runpane@latest\",\n \"npx --yes runpane@latest setup\",\n \"npx --yes runpane@latest install client\",\n \"npx --yes runpane@latest install daemon --label \\\"My Server\\\"\",\n \"pnpm dlx runpane@latest\",\n \"pnpm dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"npm i -g runpane && runpane\",\n \"npm i -g runpane && runpane setup\",\n \"npm i -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"pnpm add -g runpane && runpane\",\n \"pnpm add -g runpane && runpane setup\",\n \"pnpm add -g runpane && runpane install daemon --label \\\"My Server\\\"\",\n \"yarn dlx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"bunx runpane@latest install daemon --label \\\"My Server\\\"\"\n ],\n \"pythonCommands\": [\n \"pipx run runpane\",\n \"pipx run runpane setup\",\n \"python -m pip install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"python -m runpane setup\",\n \"runpane install daemon --label \\\"My Server\\\"\",\n \"pipx install runpane\",\n \"runpane\",\n \"runpane setup\",\n \"pipx run runpane install daemon --label \\\"My Server\\\"\",\n \"uvx runpane@latest\",\n \"uvx runpane@latest setup\",\n \"uvx runpane@latest install daemon --label \\\"My Server\\\"\",\n \"python -m runpane install daemon --label \\\"My Server\\\"\"\n ],\n \"packageManagerNotes\": [\n \"Use `pnpm dlx` for one-shot pnpm execution and `pnpm add -g` for persistent CLI installation.\",\n \"Do not document `pnpm install runpane` as the public CLI install path.\"\n ],\n \"commandUsages\": [\n \"runpane\",\n \"runpane setup\",\n \"runpane install\",\n \"runpane install client\",\n \"runpane install daemon\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\",\n \"runpane help\",\n \"runpane <command> --help\"\n ],\n \"commandDescriptions\": [\n \"`runpane` with no arguments and `runpane setup` open an interactive wizard when stdin and stdout are TTYs. In non-interactive shells or CI, both forms must print help and exit successfully instead of waiting for input.\",\n \"`runpane install` is an alias for `runpane install client`.\",\n \"`runpane install client` downloads the selected Pane desktop artifact and installs, opens, or launches it for the current platform.\",\n \"`runpane install daemon` downloads or installs Pane, resolves a stable Pane executable path, and spawns `<pane executable> --remote-setup <forwarded remote setup args>`.\",\n \"The wrapper must stream Pane stdout/stderr without reformatting because `pane --remote-setup` prints the one-time `pane-remote://...` connection code.\",\n \"`runpane update` uses the same release resolution and installer path as `install client`.\",\n \"`runpane version` prints the wrapper package version, the installed Pane version when detectable, and the latest GitHub release version when reachable.\",\n \"`runpane doctor` checks platform support, release metadata reachability, download URL selection, installed Pane detection, and remote-daemon hints.\"\n ],\n \"wrapperFlagNote\": \"The top-level `runpane --version` form prints the wrapper version. The install subcommand form `runpane install --version vX.Y.Z` selects a Pane release.\",\n \"daemonFlagNote\": \"Unknown daemon flags should be forwarded rather than dropped so newer Pane versions can extend `--remote-setup` without requiring an immediate wrapper release. Unknown flags for non-daemon commands should fail clearly.\",\n \"downloadAttribution\": [\n \"The npm package uses `source=npm` for all npm-registry consumers, including `npx`, `pnpm dlx`, `yarn dlx`, `bunx`, and global npm/pnpm installs.\",\n \"The PyPI package uses `source=pip` for all Python consumers, including pip, pipx, uvx, and `python -m runpane`.\",\n \"Wrappers should prefer `https://runpane.com/api/download?platform=<platform>&arch=<arch>&format=<format>&version=<version>&channel=<channel>&source=<npm|pip>`.\",\n \"If the website route cannot satisfy the download, wrappers may fall back to the matching GitHub release asset and print a warning that website attribution may be incomplete for that run.\"\n ],\n \"publishingCredentials\": [\n \"Local implementation, build, and dry-run validation do not need npm or PyPI API tokens.\",\n \"Release publishing should prefer npm Trusted Publishing and PyPI Trusted Publishing from GitHub Actions.\",\n \"Fallback `NPM_TOKEN` or `PYPI_API_TOKEN` credentials may be used for first package reservation or manual publication only. They must be supplied through local environment variables or GitHub Actions secrets, never committed, and revoked or rotated after use.\"\n ]\n },\n \"testFixtures\": {\n \"parserSamples\": [\n [\n \"setup\"\n ],\n [\n \"install\"\n ],\n [\n \"install\",\n \"client\",\n \"--version\",\n \"v2.2.8\",\n \"--format\",\n \"dmg\",\n \"--download-dir\",\n \"/tmp/pane-downloads\",\n \"--dry-run\",\n \"--yes\"\n ],\n [\n \"install\",\n \"daemon\",\n \"--label\",\n \"VM\",\n \"--prefer-tunnel\",\n \"ssh\",\n \"--channel\",\n \"nightly\",\n \"--base-url\",\n \"https://example.test\",\n \"--pane-dir\",\n \"/tmp/pane\",\n \"--listen-port\",\n \"4555\",\n \"--auto-listen-port\",\n \"--print-only\",\n \"--repo-ref\",\n \"main\",\n \"--unknown-future-flag\",\n \"future-value\",\n \"--dry-run\",\n \"--verbose\"\n ],\n [\n \"update\",\n \"--version\",\n \"latest\",\n \"--format\",\n \"appimage\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--dry-run\"\n ],\n [\n \"doctor\",\n \"--pane-path\",\n \"/usr/bin/pane\",\n \"--format\",\n \"zip\",\n \"--verbose\"\n ],\n [\n \"--version\"\n ]\n ],\n \"topLevelHelpIncludes\": [\n \"runpane setup\",\n \"runpane install\",\n \"runpane update\",\n \"runpane version\",\n \"runpane doctor\"\n ],\n \"npmHelpIncludes\": [\n \"pnpm dlx runpane@latest\",\n \"npx --yes runpane@latest\"\n ],\n \"pipHelpIncludes\": [\n \"pipx run runpane\",\n \"python -m pip install runpane && python -m runpane setup\"\n ],\n \"installHelpIncludes\": [\n \"--version <latest|vX.Y.Z>\",\n \"--format <auto|appimage|deb|dmg|zip|exe>\",\n \"--download-dir <path>\",\n \"--pane-path <path>\",\n \"--label <name>\",\n \"--prefer-tunnel <tailscale|ssh|manual|auto>\",\n \"--repo-ref <ref>\"\n ]\n },\n \"jsonSchemas\": {\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"ok\",\n \"error\"\n ],\n \"properties\": {\n \"ok\": {\n \"const\": false\n },\n \"error\": {\n \"type\": \"object\",\n \"required\": [\n \"message\"\n ],\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"future.repoListResult\": {\n \"type\": \"object\",\n \"description\": \"Reserved for #252 repo listing output.\"\n },\n \"future.paneCreateResult\": {\n \"type\": \"object\",\n \"description\": \"Reserved for #252 pane creation output.\"\n }\n }\n}")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes