| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
parent directory.. | ||||
Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC.
To use the SDK, you'll need:
pip install github-copilot-sdkTo include OpenTelemetry support:
pip install "github-copilot-sdk[telemetry]"Published wheels include a pinned runtime version. After installing, download the runtime:
python -m copilot download-runtimeThis caches the runtime binary locally. If you skip this step, the SDK will attempt to download it automatically on first use as a fallback.
To pre-provision the native library required by the in-process (FFI) transport (see In-process (FFI) transport), pass --in-process:
python -m copilot download-runtime --in-processThis additionally fetches the native runtime library into the versioned runtime cache. Stdio/TCP users never download it. When omitted, it is downloaded lazily on first use of the in-process transport.
| Platform | Cache path |
|---|---|
| Linux | ~/.cache/github-copilot-sdk/cli/<version>/copilot |
| macOS | ~/Library/Caches/github-copilot-sdk/cli/<version>/copilot |
| Windows | %LOCALAPPDATA%\github-copilot-sdk\cli\<version>\copilot.exe |
| Variable | Description |
|---|---|
| COPILOT_CLI_PATH | Use this specific binary instead of downloading |
| COPILOT_CLI_EXTRACT_DIR | Override the cache directory (binary placed directly here) |
| COPILOT_SKIP_CLI_DOWNLOAD | Set to 1 to disable auto-download |
| COPILOT_CLI_DOWNLOAD_BASE_URL | Override the GitHub Releases download URL |
Try the interactive chat sample (from the repo root):
cd python/samples
python chat.pyimport asyncio
from copilot import CopilotClient
from copilot.session_events import AssistantMessageData, SessionIdleData
from copilot.session import PermissionHandler
async def main():
# Client automatically starts on enter and cleans up on exit
async with CopilotClient() as client:
# Create a session with automatic cleanup
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
) as session:
# Wait for response using session.idle event
done = asyncio.Event()
def on_event(event):
match event.data:
case AssistantMessageData() as data:
print(data.content)
case SessionIdleData():
done.set()
session.on(on_event)
# Send a message and wait for completion
await session.send("What is 2+2?")
await done.wait()
asyncio.run(main())If you need more control over the lifecycle, you can call start(), stop(), and disconnect() manually:
import asyncio
from copilot import CopilotClient
from copilot.session_events import AssistantMessageData, SessionIdleData
from copilot.session import PermissionHandler
async def main():
client = CopilotClient()
await client.start()
# approve_all is only valid when managed settings are disabled.
session = await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
)
done = asyncio.Event()
def on_event(event):
match event.data:
case AssistantMessageData() as data:
print(data.content)
case SessionIdleData():
done.set()
session.on(on_event)
await session.send("What is 2+2?")
await done.wait()
# Clean up manually
await session.disconnect()
await client.stop()
asyncio.run(main())from copilot import CopilotClient
from copilot.session import PermissionHandler
async with CopilotClient() as client:
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
) as session:
def on_event(event):
print(f"Event: {event.type}")
session.on(on_event)
await session.send("Hello!")
# ... wait for events ...Note: For manual lifecycle management, see Manual Resource Management above.
from copilot import CopilotClient, RuntimeConnection
# Connect to an existing CLI server
client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:3000"))CopilotClient Constructor:
CopilotClient() # spawn the bundled runtime with defaults
CopilotClient(connection=..., log_level="debug", github_token=..., ...)All options are kw-only parameters:
RuntimeConnection variants:
Child-process connections (for_stdio/for_tcp) also expose a per-connection env field for the spawned process. Set it on the returned connection instead of the client-level env — setting both raises:
conn = RuntimeConnection.for_stdio()
conn.env = {"MY_VAR": "value"}
client = CopilotClient(connection=conn) # do NOT also pass env=... here⚠️ Experimental. The in-process transport loads the runtime's native shared library into your process and drives JSON-RPC over its C ABI (via stdlib ctypes), instead of spawning a child process.
from copilot import CopilotClient, RuntimeConnection
client = CopilotClient(connection=RuntimeConnection.for_inprocess())
await client.start()
try:
pong = await client.ping("hello")
print(pong.message)
finally:
await client.stop()Requirements & behavior:
CopilotClient.create_session():
These are passed as keyword arguments to create_session():
Session Lifecycle Methods:
# Get the session currently displayed in TUI (TUI+server mode only)
session_id = await client.get_foreground_session_id()
# Request TUI to display a specific session (TUI+server mode only)
await client.set_foreground_session_id("session-123")
# Subscribe to all lifecycle events
def on_lifecycle(event):
print(f"{event.type}: {event.session_id}")
unsubscribe = client.on_lifecycle(on_lifecycle)
# Subscribe to specific event type
unsubscribe = client.on_lifecycle(
"session.foreground", lambda e: print(f"Foreground: {e.session_id}")
)
# Later, to stop receiving events:
unsubscribe()Lifecycle Event Types:
Define tools with automatic JSON schema generation using the @define_tool decorator and Pydantic models:
from pydantic import BaseModel, Field
from copilot import CopilotClient, define_tool
class LookupIssueParams(BaseModel):
id: str = Field(description="Issue identifier")
@define_tool(description="Fetch issue details from our tracker")
async def lookup_issue(params: LookupIssueParams) -> str:
issue = await fetch_issue(params.id)
return issue.summary
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
tools=[lookup_issue],
) as session:
...Note: When using from __future__ import annotations, define Pydantic models at module level (not inside functions).
For users who prefer manual schema definition:
from copilot import CopilotClient
from copilot.tools import Tool, ToolInvocation, ToolResult
from copilot.session import PermissionHandler
async def lookup_issue(invocation: ToolInvocation) -> ToolResult:
issue_id = invocation.arguments["id"]
issue = await fetch_issue(issue_id)
return ToolResult(
text_result_for_llm=issue.summary,
result_type="success",
session_log=f"Fetched issue {issue_id}",
)
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
tools=[
Tool(
name="lookup_issue",
description="Fetch issue details from our tracker",
parameters={
"type": "object",
"properties": {
"id": {"type": "string", "description": "Issue identifier"},
},
"required": ["id"],
},
handler=lookup_issue,
)
],
) as session:
...The SDK automatically handles tool.call, executes your handler (sync or async), and responds with the final result when the tool completes. If a tool has no handler, it is exposed as a declaration only; observe external_tool.requested events and resolve the call with the pending tool RPC.
You can also create a declaration-only tool with generated Pydantic parameters:
tool = define_tool(
"lookup_issue",
description="Fetch issue details from our tracker",
params_type=LookupIssueParams,
)If you register a tool with the same name as a built-in CLI tool (e.g. edit_file, read_file), the SDK will throw an error unless you explicitly opt in by setting overrides_built_in_tool=True. This flag signals that you intend to replace the built-in tool with your custom implementation.
class EditFileParams(BaseModel):
path: str = Field(description="File path")
content: str = Field(description="New file content")
@define_tool(name="edit_file", description="Custom file editor with project-specific validation", overrides_built_in_tool=True)
async def edit_file(params: EditFileParams) -> str:
# your logicSet skip_permission=True on a tool definition to allow it to execute without triggering a permission prompt:
@define_tool(name="safe_lookup", description="A read-only lookup that needs no confirmation", skip_permission=True)
async def safe_lookup(params: LookupParams) -> str:
# your logicSet defer to control whether a tool may be loaded lazily via tool search rather than always pre-loaded. Use "auto" to allow the tool to be deferred and surfaced through tool search, or "never" to force it to always be pre-loaded. Defaults to "auto".
@define_tool(name="lookup_issue", description="Fetch issue details", defer="auto")
async def lookup_issue(params: LookupParams) -> str:
# your logicThe SDK supports image attachments via the attachments parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment:
# File attachment — runtime reads from disk
await session.send(
"What's in this image?",
attachments=[
{
"type": "file",
"path": "/path/to/image.jpg",
}
],
)
# Blob attachment — provide base64 data directly
await session.send(
"What's in this image?",
attachments=[
{
"type": "blob",
"data": base64_image_data,
"mimeType": "image/png",
}
],
)Supported image formats include JPG, PNG, GIF, and other common image types. The agent's view tool can also read images directly from the filesystem, so you can also ask questions like:
await session.send("What does the most recent jpg in this directory portray?")Enable streaming to receive assistant response chunks as they're generated:
import asyncio
from copilot import CopilotClient
from copilot.session_events import (
AssistantMessageData,
AssistantMessageDeltaData,
AssistantReasoningData,
AssistantReasoningDeltaData,
SessionIdleData,
)
from copilot.session import PermissionHandler
async def main():
async with CopilotClient() as client:
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
streaming=True,
) as session:
# Use asyncio.Event to wait for completion
done = asyncio.Event()
def on_event(event):
match event.data:
case AssistantMessageDeltaData() as data:
# Streaming message chunk - print incrementally
delta = data.delta_content or ""
print(delta, end="", flush=True)
case AssistantReasoningDeltaData() as data:
# Streaming reasoning chunk (if model supports reasoning)
delta = data.delta_content or ""
print(delta, end="", flush=True)
case AssistantMessageData() as data:
# Final message - complete content
print("\n--- Final message ---")
print(data.content)
case AssistantReasoningData() as data:
# Final reasoning content (if model supports reasoning)
print("--- Reasoning ---")
print(data.content)
case SessionIdleData():
# Session finished processing
done.set()
session.on(on_event)
await session.send("Tell me a short story")
await done.wait() # Wait for streaming to complete
asyncio.run(main())When streaming=True:
Note: assistant.message and assistant.reasoning (final events) are always sent regardless of streaming setting.
By default, sessions use infinite sessions which automatically manage context window limits through background compaction and persist state to a workspace directory.
# Default: infinite sessions enabled with default thresholds
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
) as session:
# Access the workspace path for checkpoints and files
print(session.workspace_path)
# => ~/.copilot/session-state/{session_id}/
# Custom thresholds
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
infinite_sessions={
"enabled": True,
"background_compaction_threshold": 0.80, # Start compacting at 80% context usage
"buffer_exhaustion_threshold": 0.95, # Block at 95% until compaction completes
},
) as session:
...
# Disable infinite sessions
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
infinite_sessions={"enabled": False},
) as session:
...When enabled, sessions emit compaction events:
Sessions can opt into persistent memory, allowing the agent to read and write memory across turns. Memory is configured per session and applies to both create_session and resume_session. For more background, see About GitHub Copilot Memory.
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
memory={"enabled": True},
) as session:
...When memory is omitted, no memory configuration is sent and the runtime default applies. In the default "copilot-cli" client mode the SDK leaves memory unset so the runtime applies its own default, while "empty" mode defaults memory to disabled unless you set it explicitly.
The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own Key), including local providers like Ollama. When using a custom provider, you must specify the model explicitly.
ProviderConfig fields:
Example with Ollama:
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="deepseek-coder-v2:16b", # Required when using custom provider
provider={
"type": "openai",
"base_url": "http://localhost:11434/v1", # Ollama endpoint
# api_key not required for Ollama
},
) as session:
await session.send("Hello!")Example with custom OpenAI-compatible API:
import os
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-4",
provider={
"type": "openai",
"base_url": "https://my-api.example.com/v1",
"api_key": os.environ["MY_API_KEY"],
},
) as session:
...Example with Azure OpenAI:
import os
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-4",
provider={
"type": "azure", # Must be "azure" for Azure endpoints, NOT "openai"
"base_url": "https://my-resource.openai.azure.com", # Just the host, no path
"api_key": os.environ["AZURE_OPENAI_KEY"],
"azure": {
"api_version": "2024-10-21",
},
},
) as session:
...Important notes:
- When using a custom provider, the model parameter is required. The SDK will throw an error if no model is specified.
- For Azure OpenAI endpoints (*.openai.azure.com), you must use type: "azure", not type: "openai".
- The base_url should be just the host (e.g., https://my-resource.openai.azure.com). Do not include /openai/v1 in the URL - the SDK handles path construction automatically.
Control the system prompt using system_message in session config:
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
system_message={
"mode": "append",
"content": """
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
""",
},
) as session:
...Use mode: "customize" to selectively override individual sections of the prompt while preserving the rest:
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
system_message={
"mode": "customize",
"sections": {
"tone": {
"action": "replace",
"content": "Respond in a warm, professional tone. Be thorough in explanations.",
},
"code_change_rules": {"action": "remove"},
"guidelines": {"action": "append", "content": "\n* Always cite data sources"},
},
"content": "Focus on financial analysis and reporting.",
},
) as session:
...Available section IDs: "preamble", "identity", "tone", "tool_efficiency", "environment_context", "code_change_rules", "guidelines", "safety", "tool_instructions", "custom_instructions", "runtime_instructions", "last_instructions". "identity" and "tool_instructions" are section groups that target a collection of related sub-sections as a unit; use "preamble" to target just the identity preamble.
Each section override supports five string actions: "replace", "remove", "append", "prepend", and "preserve" (a no-op that opts an individually-addressable section out of a group-level "remove"). Unknown section IDs are handled gracefully: content from "replace"/"append"/"prepend" overrides is appended to additional instructions, and "remove" overrides are silently ignored.
You can also pass a transform callback as the action instead of a string. The callback receives the current section content and returns the new content (sync or async):
def redact_paths(content: str) -> str:
return content.replace("/home/user", "/***")
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
system_message={
"mode": "customize",
"sections": {
"environment_context": {"action": redact_paths},
},
},
) as session:
...For full control (removes all SDK guardrails including security restrictions), use mode: "replace":
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
system_message={
"mode": "replace",
"content": "You are a helpful assistant.",
},
) as session:
...The SDK supports OpenTelemetry for distributed tracing. Provide a telemetry config to enable trace export and automatic W3C Trace Context propagation.
from copilot import CopilotClient
client = CopilotClient(
telemetry={
"otlp_endpoint": "http://localhost:4318",
},
)TelemetryConfig options:
Trace context (traceparent/tracestate) is automatically propagated between the SDK and CLI on create_session, resume_session, and send calls, and inbound when the CLI invokes tool handlers.
Install with telemetry extras: pip install "github-copilot-sdk[telemetry]" (provides opentelemetry-api)
An on_permission_request handler is optional when you create or resume a session. When provided, it is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and returns a decision. When omitted, permission requests are emitted as events and left pending for the consumer to resolve with the pending permission RPC.
Use the built-in PermissionHandler.approve_all helper to approve ordinary permission requests automatically:
from copilot import CopilotClient
from copilot.session import PermissionHandler
session = await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
)When enable_managed_settings is true for the session, approve_all raises an error. Use a custom handler for managed sessions; request-level managed_approval_required remains available for human-facing confirmation logic.
Provide your own function to inspect each request and apply custom logic (sync or async). Check managed_approval_required before any automatic approval:
from copilot import PermissionNoResult, PermissionRequest, PermissionRequestResult
from copilot.rpc import (
PermissionDecisionApproveOnce,
PermissionDecisionReject,
)
from copilot.session_events import PermissionRequestShell
def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
if getattr(request, "managed_approval_required", False) is True:
return PermissionNoResult()
# ``PermissionRequest`` is a discriminated union — pattern-match on
# the variant class to access the per-kind fields.
match request:
case PermissionRequestShell(full_command_text=cmd):
# Deny shell commands
return PermissionDecisionReject(feedback=f"Shell denied: {cmd}")
case _:
return PermissionDecisionApproveOnce()
session = await client.create_session(
on_permission_request=on_permission_request,
model="gpt-5",
)Async handlers are also supported:
async def on_permission_request(
request: PermissionRequest, invocation: dict
) -> PermissionRequestResult:
if getattr(request, "managed_approval_required", False) is True:
return PermissionNoResult()
# Simulate an async approval check (e.g., prompting a user over a network)
await asyncio.sleep(0)
return PermissionDecisionApproveOnce()The handler returns a PermissionRequestResult, which is an alias for PermissionDecision | PermissionNoResult (the generated wire-level union of every decision variant, plus a sentinel that suppresses this SDK client's response). Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on permission.completed session events.
| Variant | Meaning |
|---|---|
| PermissionDecisionApproveOnce() | Allow this single request |
| PermissionDecisionReject(feedback="…") | Deny the request (optional feedback string forwarded to the LLM) |
| PermissionDecisionUserNotAvailable() | Deny the request because no user is available to confirm it (the default) |
| PermissionNoResult() | During event-based dispatch, suppress this SDK client's response so another connected client can answer the pending request; legacy direct callbacks cannot abstain |
Several richer variants (PermissionDecisionApproveForSession, PermissionDecisionApproveForLocation, PermissionDecisionApprovePermanently, …) are available for granting longer-lived approvals; see the generated copilot.rpc module for the full list.
You may pass on_permission_request when resuming a session too:
session = await client.resume_session(
"session-id",
on_permission_request=PermissionHandler.approve_all,
)To let a specific custom tool bypass the permission prompt entirely, set skip_permission=True on the tool definition. See Skipping Permission Prompts under Tools.
Enable the agent to ask questions to the user using the ask_user tool by providing an on_user_input_request handler:
async def handle_user_input(request, invocation):
# request["question"] - The question to ask
# request.get("choices") - Optional list of choices for multiple choice
# request.get("allowFreeform", True) - Whether freeform input is allowed
print(f"Agent asks: {request['question']}")
if request.get("choices"):
print(f"Choices: {', '.join(request['choices'])}")
# Return the user's response
return {
"answer": "User's answer here",
"wasFreeform": True, # Whether the answer was freeform (not from choices)
}
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
on_user_input_request=handle_user_input,
) as session:
...Hook into session lifecycle events by providing handlers in the hooks configuration:
async def on_pre_tool_use(input, invocation):
print(f"About to run tool: {input['toolName']}")
# Return permission decision and optionally modify args
return {
"permissionDecision": "allow", # "allow", "deny", or "ask"
"modifiedArgs": input.get("toolArgs"), # Optionally modify tool arguments
"additionalContext": "Extra context for the model",
}
async def on_post_tool_use(input, invocation):
print(f"Tool {input['toolName']} completed")
return {
"additionalContext": "Post-execution notes",
}
async def on_post_tool_use_failure(input, invocation):
# Fires when a tool's result was a failure. `on_post_tool_use` only fires
# on success, so register this handler to observe failed tool calls. The
# CLI extracts the failure message and passes it as the `error` field.
print(f"Tool {input['toolName']} failed: {input['error']}")
return {
"additionalContext": f"Retry guidance for {input['toolName']}",
}
async def on_user_prompt_submitted(input, invocation):
print(f"User prompt: {input['prompt']}")
return {
"modifiedPrompt": input["prompt"], # Optionally modify the prompt
}
async def on_session_start(input, invocation):
print(f"Session started from: {input['source']}") # "startup", "resume", "new"
return {
"additionalContext": "Session initialization context",
}
async def on_session_end(input, invocation):
print(f"Session ended: {input['reason']}")
async def on_error_occurred(input, invocation):
print(f"Error in {input['errorContext']}: {input['error']}")
return {
"errorHandling": "retry", # "retry", "skip", or "abort"
}
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
hooks={
"on_pre_tool_use": on_pre_tool_use,
"on_post_tool_use": on_post_tool_use,
"on_post_tool_use_failure": on_post_tool_use_failure,
"on_user_prompt_submitted": on_user_prompt_submitted,
"on_session_start": on_session_start,
"on_session_end": on_session_end,
"on_error_occurred": on_error_occurred,
},
) as session:
...Available hooks:
Register slash commands that users can invoke from the CLI TUI. When the user types /commandName, the SDK dispatches the event to your handler.
from copilot.session import CommandDefinition, CommandContext, PermissionHandler
async def handle_deploy(ctx: CommandContext) -> None:
print(f"Deploying with args: {ctx.args}")
# ctx.session_id — the session where the command was invoked
# ctx.command — full command text (e.g. "/deploy production")
# ctx.command_name — command name without leading / (e.g. "deploy")
# ctx.args — raw argument string (e.g. "production")
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
commands=[
CommandDefinition(
name="deploy",
description="Deploy the app",
handler=handle_deploy,
),
CommandDefinition(
name="rollback",
description="Rollback to previous version",
handler=lambda ctx: print("Rolling back..."),
),
],
) as session:
...Commands can also be provided when resuming a session via resume_session(commands=[...]).
The session.ui API provides convenience methods for asking the user questions through interactive dialogs. These methods are only available when the CLI host supports elicitation — check session.capabilities before calling.
ui_caps = session.capabilities.get("ui", {})
if ui_caps.get("elicitation"):
# Safe to call session.ui methods
...Shows a yes/no confirmation dialog:
ok = await session.ui.confirm("Deploy to production?")
if ok:
print("Deploying...")Shows a selection dialog with a list of options:
env = await session.ui.select("Choose environment:", ["staging", "production", "dev"])
if env:
print(f"Selected: {env}")Shows a text input dialog with optional constraints:
name = await session.ui.input("Enter your name:")
# With options
email = await session.ui.input(
"Enter email:",
{
"title": "Email Address",
"description": "We'll use this for notifications",
"format": "email",
},
)For full control, use the elicitation() method with a custom JSON schema:
result = await session.ui.elicitation(
{
"message": "Configure deployment",
"requestedSchema": {
"type": "object",
"properties": {
"region": {"type": "string", "enum": ["us-east-1", "eu-west-1"]},
"replicas": {"type": "number", "minimum": 1, "maximum": 10},
},
"required": ["region"],
},
}
)
if result["action"] == "accept":
region = result["content"]["region"]
replicas = result["content"].get("replicas", 1)When the server (or an MCP tool) needs to ask the end-user a question, it sends an elicitation.requested event. Provide an on_elicitation_request handler to respond:
from copilot.session import ElicitationContext, ElicitationResult, PermissionHandler
async def handle_elicitation(
context: ElicitationContext,
) -> ElicitationResult:
# context["session_id"] — the session ID
# context["message"] — what the server is asking
# context.get("requestedSchema") — optional JSON schema for form fields
# context.get("mode") — "form" or "url"
print(f"Server asks: {context['message']}")
# Return the user's response
return {
"action": "accept", # or "decline" or "cancel"
"content": {"answer": "yes"},
}
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
on_elicitation_request=handle_elicitation,
) as session:
...When on_elicitation_request is provided, the SDK automatically:
Install uv and a supported Node.js version, then from the repository root:
cd nodejs
npm cicd test/harness
npm cicd python
uv sync
uv run pytest| Back | FazBrowse Home | New Git URL |