Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
541
extensions/commander-mcp.ts
Normal file
541
extensions/commander-mcp.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
// ABOUTME: Bridge extension that exposes Commander MCP tools as native Pi tools.
|
||||
// ABOUTME: Spawns commander-mcp as a subprocess and proxies JSON-RPC calls over stdio.
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { McpClient } from "./lib/mcp-client.ts";
|
||||
import { createReadyGate, resolveGate, resetGate } from "./lib/commander-ready.ts";
|
||||
|
||||
// ── Configuration ───────────────────────────────────────────────────
|
||||
|
||||
const SERVER_PATH = "/Users/ricardo/Workshop/Github-Work/commander/services/commander-mcp/dist/server.js";
|
||||
const SERVER_ENV: Record<string, string> = {
|
||||
COMMANDER_WS_URL: process.env.COMMANDER_WS_URL || "ws://localhost:9002",
|
||||
JIRA_URL: process.env.JIRA_URL || "",
|
||||
JIRA_EMAIL: process.env.JIRA_EMAIL || "",
|
||||
JIRA_API_TOKEN: process.env.JIRA_API_TOKEN || "",
|
||||
AGENTMAIL_API_KEY: process.env.AGENTMAIL_API_KEY || "",
|
||||
};
|
||||
|
||||
// ── Tool definitions ────────────────────────────────────────────────
|
||||
|
||||
const TOOLS: { name: string; label: string; description: string }[] = [
|
||||
{
|
||||
name: "commander_task",
|
||||
label: "Commander Task",
|
||||
description: `Unified task management - create, track, and execute work items.
|
||||
|
||||
OPERATIONS BY CATEGORY:
|
||||
|
||||
TASK CRUD:
|
||||
- "create": Start new task (requires description, working_directory)
|
||||
- "get": Get task details by task_id
|
||||
- "update": Modify task fields
|
||||
- "list": Find tasks with filters (status, agent_id, working_directory)
|
||||
|
||||
LIFECYCLE (state transitions):
|
||||
- "claim": Start working on pending task (validates working_directory match)
|
||||
- "complete": Mark success with result summary
|
||||
- "fail": Mark failure with error_message
|
||||
|
||||
GROUPS (batch operations):
|
||||
- "group:create": Create task group with multiple tasks (requires group_name, tasks[], initiative_summary, total_waves)
|
||||
- "group:get": Get group details and progress percentage
|
||||
- "group:list": List all groups (no group_id) or tasks in a group (with group_id)
|
||||
- "group:update": Update wave progress and overall_status
|
||||
|
||||
COMMENTS & LOGS:
|
||||
- "comment:add": Add progress/error/handoff comment (ALWAYS include agent_name!)
|
||||
- "comment:list": View task comments
|
||||
- "log": Add real-time dashboard log entry
|
||||
|
||||
POLICY:
|
||||
- "policy:update": Modify task execution policy (Warden-compatible)
|
||||
|
||||
TASK WORKFLOW:
|
||||
1. Create task → status='pending'
|
||||
2. Claim task → status='working', validates working_directory
|
||||
3. Complete/Fail → status='completed' or 'failed'
|
||||
|
||||
EXAMPLE - Create and claim a task:
|
||||
{ "operation": "create", "description": "Fix auth bug in login.ts", "working_directory": "/project/src" }
|
||||
{ "operation": "claim", "task_id": 123, "agent_name": "claude" }
|
||||
|
||||
EXAMPLE - Create task group:
|
||||
{ "operation": "group:create", "group_name": "Auth Refactor", "initiative_summary": "Migrate JWT to OAuth", "total_waves": 3, "working_directory": "/project", "tasks": [{"description": "...", "task_prompt": "...", "dependency_order": 0, "context": "..."}] }`,
|
||||
},
|
||||
{
|
||||
name: "commander_session",
|
||||
label: "Commander Session",
|
||||
description: `Unified session and terminal management - track agents and UI state.
|
||||
|
||||
OPERATIONS BY CATEGORY:
|
||||
|
||||
SESSION MANAGEMENT:
|
||||
- "create": Start new session (requires name)
|
||||
- "get": Get session by session_id
|
||||
- "list": List sessions (filter by working_directory, status)
|
||||
|
||||
TERMINAL OPERATIONS:
|
||||
- "terminals:list": List active terminal processes (filter by cli_type, status)
|
||||
- "pipe": Send text to terminal (requires terminal_session_id, data)
|
||||
|
||||
CLEANUP (housekeeping):
|
||||
- "cleanup:status": Check stale session counts and get recommendation
|
||||
- "cleanup:stale": Remove sessions older than min_age_hours (default 24h)
|
||||
- "cleanup:terminate": End specific session by session_id
|
||||
- "cleanup:self": Clean up calling agent's own session
|
||||
|
||||
FILE VIEWER (Commander UI):
|
||||
- "file:open": Display file in floating window (requires file_path, supports line_range)
|
||||
- "file:close": Close viewer by viewer_id
|
||||
|
||||
BEST PRACTICES FOR AGENTS:
|
||||
1. At start of work: Call cleanup:status to check session health
|
||||
2. If >10 stale sessions: Call cleanup:stale to clean up
|
||||
3. When work is DONE: Call cleanup:self to clean up your own session
|
||||
|
||||
EXAMPLE - Cleanup workflow:
|
||||
{ "operation": "cleanup:status" }
|
||||
{ "operation": "cleanup:stale", "min_age_hours": 24 }`,
|
||||
},
|
||||
{
|
||||
name: "commander_workflow",
|
||||
label: "Commander Workflow",
|
||||
description: `Access development workflow documentation, templates, and standards.
|
||||
|
||||
WORKFLOWS: "kiro", "contextos"
|
||||
|
||||
OPERATIONS:
|
||||
- "doc:get": Retrieve instruction document (requires workflow, doc_type)
|
||||
- "doc:list": List available doc types for a workflow
|
||||
- "doc:search": Search instructions by query (requires query)
|
||||
- "template:get": Get template content (requires workflow, template_type)
|
||||
- "template:list": List available templates for a workflow
|
||||
- "steering:get": Get steering document (requires steering_type) - Kiro only
|
||||
- "steering:list": List available steering documents - Kiro only
|
||||
|
||||
EXAMPLE - Get Kiro guidelines:
|
||||
{ "operation": "doc:get", "workflow": "kiro", "doc_type": "guidelines" }`,
|
||||
},
|
||||
{
|
||||
name: "commander_spec",
|
||||
label: "Commander Spec",
|
||||
description: `Manage development specs - structured feature specifications for spec-driven development.
|
||||
|
||||
OPERATIONS:
|
||||
- "create": Start new spec (requires name, description, project_id)
|
||||
- "get": Get spec details by spec_id
|
||||
- "list": List all specs
|
||||
- "update": Modify spec status
|
||||
- "shape": Initiate AI shaping (requires spec_id, feature_idea)
|
||||
- "write": Generate requirements from shaped content (requires spec_id, shaped_content)
|
||||
- "create_tasks": Convert spec to executable tasks (requires spec_id, selected_tasks[])
|
||||
|
||||
SPEC WORKFLOW:
|
||||
1. CREATE → 2. SHAPE → 3. WRITE → 4. CREATE_TASKS
|
||||
|
||||
EXAMPLE - Start shaping a feature:
|
||||
{ "operation": "shape", "spec_id": 1, "feature_idea": "Add OAuth login with Google and GitHub providers" }`,
|
||||
},
|
||||
{
|
||||
name: "commander_jira",
|
||||
label: "Commander Jira",
|
||||
description: `Interact with Jira issues - get details, update status, add comments, and link PRs.
|
||||
|
||||
OPERATIONS BY CATEGORY:
|
||||
|
||||
ISSUE OPERATIONS:
|
||||
- "issue:get": Get issue details (requires issue_key)
|
||||
- "issue:update": Update issue fields (requires issue_key, plus fields to update)
|
||||
- "issue:search": Search using JQL (requires jql)
|
||||
|
||||
TRANSITION OPERATIONS:
|
||||
- "transition:list": List available transitions for issue (requires issue_key)
|
||||
- "transition:execute": Change issue status (requires issue_key + transition_id OR transition_name)
|
||||
|
||||
COMMENT OPERATIONS:
|
||||
- "comment:add": Add comment to issue (requires issue_key, body)
|
||||
- "comment:list": List issue comments (requires issue_key)
|
||||
|
||||
LINK OPERATIONS:
|
||||
- "link:pr": Link PR to issue via formatted comment (requires issue_key, pr_url)
|
||||
|
||||
STATUS OPERATIONS:
|
||||
- "status:check": Check Jira connection status
|
||||
|
||||
EXAMPLE - Start working on issue:
|
||||
{ "operation": "issue:get", "issue_key": "PROJ-123" }
|
||||
{ "operation": "transition:execute", "issue_key": "PROJ-123", "transition_name": "In Progress" }`,
|
||||
},
|
||||
{
|
||||
name: "commander_mailbox",
|
||||
label: "Commander Mailbox",
|
||||
description: `Inter-agent messaging and status broadcasting for Commander dashboard visibility.
|
||||
|
||||
IMPORTANT: ALL agents MUST send status updates at key milestones.
|
||||
|
||||
OPERATIONS:
|
||||
- "send": Send a message (requires from_agent, to_agent, body)
|
||||
- "inbox": Get inbox messages (requires agent_name)
|
||||
- "outbox": Get sent messages (requires agent_name)
|
||||
- "read": Mark message read (requires message_id)
|
||||
- "read_all": Mark all read (requires agent_name)
|
||||
- "thread": Get thread messages (requires thread_id)
|
||||
- "unread_count": Get unread count (requires agent_name)
|
||||
- "delete": Delete a message (requires message_id)
|
||||
|
||||
MESSAGE TYPES: status, question, result, error, dispatch, escalation, health_check, worker_done, merge_ready
|
||||
PRIORITY: low, normal, high, urgent
|
||||
BROADCAST GROUPS: @all, @builders, @scouts, @reviewers, @leads, @coordinators
|
||||
|
||||
EXAMPLE - Status update:
|
||||
{ "operation": "send", "from_agent": "agent-task-42", "to_agent": "commander", "body": "Starting implementation", "message_type": "status", "task_id": 42 }`,
|
||||
},
|
||||
{
|
||||
name: "commander_orchestration",
|
||||
label: "Commander Orchestration",
|
||||
description: `Agent registry and orchestration for hierarchical multi-agent coordination.
|
||||
|
||||
OPERATIONS:
|
||||
- "agent:register": Register a new agent (requires name, agent_type)
|
||||
- "agent:deregister": Remove an agent (requires agent_id)
|
||||
- "agent:list": List registered agents (optional: active_only)
|
||||
- "agent:heartbeat": Record agent heartbeat (requires agent_id)
|
||||
- "agent:hierarchy": Get agent hierarchy tree (optional: parent_agent_id)
|
||||
- "agent:get_by_name": Find agent by name (requires name)
|
||||
- "agent:find_capable": Find agents with a capability (requires capability)
|
||||
- "agent:state": Update agent state (requires agent_id, state)
|
||||
- "dispatch": Assign a task to an agent (requires agent_id, task_id)
|
||||
- "health:check": Check for stale/zombie agents (optional: threshold_secs)
|
||||
|
||||
HIERARCHY RULES:
|
||||
- Coordinator (depth 0) → Leads (depth 1) → Workers (depth 2)
|
||||
- Max 25 concurrent agents
|
||||
AGENT STATES: idle, spawning, running, working, stuck, done, stopped, dead
|
||||
|
||||
EXAMPLE:
|
||||
{ "operation": "agent:register", "name": "worker-1", "agent_type": "claude", "role": "worker" }
|
||||
{ "operation": "dispatch", "agent_id": 1, "task_id": 42 }`,
|
||||
},
|
||||
{
|
||||
name: "commander_dependency",
|
||||
label: "Commander Dependency",
|
||||
description: `Task dependency graph management for coordinating execution order.
|
||||
|
||||
OPERATIONS:
|
||||
- "add": Create dependency between tasks (requires from_task_id, to_task_id)
|
||||
- "remove": Delete dependency by ID (requires dependency_id)
|
||||
- "remove_by_edge": Delete dependency by edge (requires from_task_id, to_task_id)
|
||||
- "get": Get all dependencies for a task (requires task_id)
|
||||
- "blockers": Get tasks that block this task (requires task_id)
|
||||
- "dependents": Get tasks that depend on this task (requires task_id)
|
||||
- "ready_tasks": Get tasks ready to work on (no open blocking deps)
|
||||
- "blocked_tasks": Get tasks currently blocked
|
||||
- "graph": Get full dependency graph (optional: group_id)
|
||||
- "rebuild_cache": Rebuild transitive blocking cache
|
||||
- "cached_blockers": Get cached blockers for a task (requires task_id)
|
||||
|
||||
DEPENDENCY TYPES: blocks, parent_child, waits_for, related, conditional_blocks
|
||||
|
||||
EXAMPLE - Create blocking dependency:
|
||||
{ "operation": "add", "from_task_id": 1, "to_task_id": 2, "dependency_type": "blocks" }
|
||||
|
||||
EXAMPLE - Find ready work:
|
||||
{ "operation": "ready_tasks" }`,
|
||||
},
|
||||
{
|
||||
name: "commander_agentmail",
|
||||
label: "Commander AgentMail",
|
||||
description: `Send emails via AgentMail — email reports, briefings, and custom messages.
|
||||
|
||||
Sends emails from the "Commander Assistant" inbox via AgentMail API.
|
||||
Default recipient: ruizrica2@gmail.com
|
||||
|
||||
OPERATIONS:
|
||||
- "send:report": Email a generated report (requires content, optional report_name)
|
||||
- "send:briefing": Email a morning briefing (requires content)
|
||||
- "send:custom": Send a custom email (requires subject + content)
|
||||
- "status:check": Check AgentMail connection and inbox status
|
||||
|
||||
Content supports markdown (auto-converted to styled HTML), raw HTML, or plain text.
|
||||
|
||||
EXAMPLE - Send a report:
|
||||
{ "operation": "send:report", "report_name": "Weekly Code Review", "content": "# Report\\n..." }
|
||||
|
||||
EXAMPLE - Send custom email:
|
||||
{ "operation": "send:custom", "subject": "Task Update", "content": "The refactor is complete..." }
|
||||
|
||||
EXAMPLE - Check status:
|
||||
{ "operation": "status:check" }`,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Per-tool schemas ────────────────────────────────────────────────
|
||||
// Each tool gets a schema that explicitly defines its parameters so that
|
||||
// the model knows what to send. additionalProperties remains true for
|
||||
// forward-compatibility with new fields.
|
||||
|
||||
const TaskParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform" }),
|
||||
// CRUD
|
||||
description: Type.Optional(Type.String({ description: "Task description (for create)" })),
|
||||
working_directory: Type.Optional(Type.String({ description: "Working directory path (for create, list)" })),
|
||||
task_id: Type.Optional(Type.Number({ description: "Task ID (for get, update, claim, complete, fail)" })),
|
||||
status: Type.Optional(Type.String({ description: "Task status: pending, working, completed, failed, cancelled (for update, list)" })),
|
||||
agent_id: Type.Optional(Type.Number({ description: "Agent ID (for list filter)" })),
|
||||
// Lifecycle
|
||||
agent_name: Type.Optional(Type.String({ description: "Agent name (for claim)" })),
|
||||
result: Type.Optional(Type.String({ description: "Result summary (for complete)" })),
|
||||
error_message: Type.Optional(Type.String({ description: "Error message (for fail)" })),
|
||||
// Groups
|
||||
group_name: Type.Optional(Type.String({ description: "Group name (for group:create)" })),
|
||||
group_id: Type.Optional(Type.Number({ description: "Group ID (for group:get, group:list, group:update)" })),
|
||||
initiative_summary: Type.Optional(Type.String({ description: "Initiative summary (for group:create)" })),
|
||||
total_waves: Type.Optional(Type.Number({ description: "Total waves (for group:create)" })),
|
||||
tasks: Type.Optional(Type.Array(Type.Object({
|
||||
description: Type.String(),
|
||||
task_prompt: Type.Optional(Type.String()),
|
||||
dependency_order: Type.Optional(Type.Number()),
|
||||
context: Type.Optional(Type.String()),
|
||||
}), { description: "Array of task definitions (for group:create)" })),
|
||||
overall_status: Type.Optional(Type.String({ description: "Overall group status (for group:update)" })),
|
||||
// Comments & Logs
|
||||
body: Type.Optional(Type.String({ description: "Comment body (for comment:add)" })),
|
||||
message: Type.Optional(Type.String({ description: "Log message (for log)" })),
|
||||
// Policy
|
||||
policy: Type.Optional(Type.Object({}, { additionalProperties: true, description: "Policy object (for policy:update)" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
const SessionParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform" }),
|
||||
name: Type.Optional(Type.String({ description: "Session name (for create)" })),
|
||||
session_id: Type.Optional(Type.Number({ description: "Session ID (for get, cleanup:terminate)" })),
|
||||
working_directory: Type.Optional(Type.String({ description: "Working directory filter (for list)" })),
|
||||
status: Type.Optional(Type.String({ description: "Status filter (for list)" })),
|
||||
// Terminal
|
||||
terminal_session_id: Type.Optional(Type.Number({ description: "Terminal session ID (for pipe)" })),
|
||||
cli_type: Type.Optional(Type.String({ description: "CLI type filter (for terminals:list)" })),
|
||||
data: Type.Optional(Type.String({ description: "Text to send to terminal (for pipe)" })),
|
||||
// File viewer
|
||||
file_path: Type.Optional(Type.String({ description: "File path to open (for file:open)" })),
|
||||
line_range: Type.Optional(Type.String({ description: "Line range like '45-60' (for file:open)" })),
|
||||
viewer_id: Type.Optional(Type.Number({ description: "Viewer ID (for file:close)" })),
|
||||
// Cleanup
|
||||
min_age_hours: Type.Optional(Type.Number({ description: "Minimum age in hours for stale cleanup (for cleanup:stale)" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
const WorkflowParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform" }),
|
||||
workflow: Type.Optional(Type.String({ description: "Workflow name: kiro, contextos (for doc:get, doc:list, template:get, template:list)" })),
|
||||
doc_type: Type.Optional(Type.String({ description: "Document type (for doc:get)" })),
|
||||
query: Type.Optional(Type.String({ description: "Search query (for doc:search)" })),
|
||||
template_type: Type.Optional(Type.String({ description: "Template type (for template:get)" })),
|
||||
steering_type: Type.Optional(Type.String({ description: "Steering document type (for steering:get)" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
const SpecParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform" }),
|
||||
spec_id: Type.Optional(Type.Number({ description: "Spec ID (for get, update, shape, write, create_tasks)" })),
|
||||
name: Type.Optional(Type.String({ description: "Spec name (for create)" })),
|
||||
description: Type.Optional(Type.String({ description: "Spec description (for create)" })),
|
||||
project_id: Type.Optional(Type.Number({ description: "Project ID (for create)" })),
|
||||
feature_idea: Type.Optional(Type.String({ description: "Feature idea text (for shape)" })),
|
||||
shaped_content: Type.Optional(Type.String({ description: "Shaped content (for write)" })),
|
||||
selected_tasks: Type.Optional(Type.Array(Type.Unknown(), { description: "Selected tasks to create (for create_tasks)" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
const JiraParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform" }),
|
||||
issue_key: Type.Optional(Type.String({ description: "Jira issue key like PROJ-123 (for most operations)" })),
|
||||
jql: Type.Optional(Type.String({ description: "JQL query string (for issue:search)" })),
|
||||
body: Type.Optional(Type.String({ description: "Comment body (for comment:add)" })),
|
||||
pr_url: Type.Optional(Type.String({ description: "PR URL to link (for link:pr)" })),
|
||||
transition_id: Type.Optional(Type.Number({ description: "Transition ID (for transition:execute)" })),
|
||||
transition_name: Type.Optional(Type.String({ description: "Transition name (for transition:execute)" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
const MailboxParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform" }),
|
||||
from_agent: Type.Optional(Type.String({ description: "Sender agent name (for send)" })),
|
||||
to_agent: Type.Optional(Type.String({ description: "Recipient agent name or broadcast group (for send)" })),
|
||||
agent_name: Type.Optional(Type.String({ description: "Agent name (for inbox, outbox, read_all, unread_count)" })),
|
||||
body: Type.Optional(Type.String({ description: "Message body (for send)" })),
|
||||
message_type: Type.Optional(Type.String({ description: "Message type: status, question, result, error, dispatch, escalation (for send)" })),
|
||||
message_id: Type.Optional(Type.Number({ description: "Message ID (for read, delete)" })),
|
||||
thread_id: Type.Optional(Type.Number({ description: "Thread ID (for thread)" })),
|
||||
task_id: Type.Optional(Type.Number({ description: "Related task ID (for send)" })),
|
||||
priority: Type.Optional(Type.String({ description: "Priority: low, normal, high, urgent (for send)" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
const OrchestrationParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform" }),
|
||||
name: Type.Optional(Type.String({ description: "Agent name (for agent:register, agent:get_by_name)" })),
|
||||
agent_type: Type.Optional(Type.String({ description: "Agent type (for agent:register)" })),
|
||||
role: Type.Optional(Type.String({ description: "Agent role: coordinator, lead, worker (for agent:register)" })),
|
||||
agent_id: Type.Optional(Type.Number({ description: "Agent ID (for agent:deregister, agent:heartbeat, agent:state, dispatch)" })),
|
||||
agent_name: Type.Optional(Type.String({ description: "Agent name for heartbeat (for agent:heartbeat)" })),
|
||||
task_id: Type.Optional(Type.Number({ description: "Task ID (for dispatch)" })),
|
||||
state: Type.Optional(Type.String({ description: "Agent state: idle, running, working, stuck, done, stopped (for agent:state)" })),
|
||||
capability: Type.Optional(Type.String({ description: "Capability to search for (for agent:find_capable)" })),
|
||||
parent_agent_id: Type.Optional(Type.Number({ description: "Parent agent ID (for agent:hierarchy)" })),
|
||||
active_only: Type.Optional(Type.Boolean({ description: "Filter to active agents only (for agent:list)" })),
|
||||
threshold_secs: Type.Optional(Type.Number({ description: "Staleness threshold in seconds (for health:check)" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
const DependencyParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform" }),
|
||||
task_id: Type.Optional(Type.Number({ description: "Task ID (for get, blockers, dependents, cached_blockers)" })),
|
||||
from_task_id: Type.Optional(Type.Number({ description: "Source task ID (for add, remove_by_edge)" })),
|
||||
to_task_id: Type.Optional(Type.Number({ description: "Target task ID (for add, remove_by_edge)" })),
|
||||
dependency_id: Type.Optional(Type.Number({ description: "Dependency ID (for remove)" })),
|
||||
dependency_type: Type.Optional(Type.String({ description: "Dependency type: blocks, parent_child, waits_for, related (for add)" })),
|
||||
group_id: Type.Optional(Type.Number({ description: "Group ID (for graph)" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
const AgentMailParams = Type.Object({
|
||||
operation: Type.String({ description: "Operation to perform: send:report, send:briefing, send:custom, status:check" }),
|
||||
to: Type.Optional(Type.String({ description: "Recipient email address (default: ruizrica2@gmail.com)" })),
|
||||
subject: Type.Optional(Type.String({ description: "Email subject line (for send:custom, or override for send:report)" })),
|
||||
content: Type.Optional(Type.String({ description: "Email content — markdown, HTML, or plain text" })),
|
||||
report_name: Type.Optional(Type.String({ description: "Report name (for send:report — used in subject line)" })),
|
||||
format: Type.Optional(Type.String({ description: "Content format: markdown (default), html, text" })),
|
||||
}, { additionalProperties: true });
|
||||
|
||||
// Map tool names to their specific parameter schemas
|
||||
const TOOL_PARAMS: Record<string, ReturnType<typeof Type.Object>> = {
|
||||
commander_task: TaskParams,
|
||||
commander_session: SessionParams,
|
||||
commander_workflow: WorkflowParams,
|
||||
commander_spec: SpecParams,
|
||||
commander_jira: JiraParams,
|
||||
commander_mailbox: MailboxParams,
|
||||
commander_orchestration: OrchestrationParams,
|
||||
commander_dependency: DependencyParams,
|
||||
commander_agentmail: AgentMailParams,
|
||||
};
|
||||
|
||||
// ── Extension entry point ───────────────────────────────────────────
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
const client = new McpClient(SERVER_PATH, SERVER_ENV);
|
||||
const g = globalThis as any;
|
||||
let healthCheckTimer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
// ── Ready gate — queues ops until probe resolves ────────────────
|
||||
const gate = createReadyGate();
|
||||
g.__piCommanderGate = gate;
|
||||
g.__piCommanderOnReady = g.__piCommanderOnReady || [];
|
||||
|
||||
// Helper: drain queued ops after gate resolves to available
|
||||
function drainGateQueue(ops: { fn: (client: any) => Promise<void>; label: string }[]): void {
|
||||
for (const op of ops) {
|
||||
op.fn(client).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: drain onReady callbacks registered by other extensions
|
||||
function drainOnReadyCallbacks(): void {
|
||||
const cbs: Array<() => void> = g.__piCommanderOnReady || [];
|
||||
g.__piCommanderOnReady = [];
|
||||
for (const cb of cbs) {
|
||||
try { cb(); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: ensure connected before calling
|
||||
async function ensureConnected(): Promise<void> {
|
||||
if (!client.isConnected()) {
|
||||
await client.connect();
|
||||
}
|
||||
}
|
||||
|
||||
// Register all 8 tools
|
||||
for (const tool of TOOLS) {
|
||||
pi.registerTool({
|
||||
name: tool.name,
|
||||
label: tool.label,
|
||||
description: tool.description,
|
||||
parameters: TOOL_PARAMS[tool.name] || TaskParams,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
try {
|
||||
await ensureConnected();
|
||||
const isLightweight = tool.name === "commander_mailbox";
|
||||
const timeoutMs = isLightweight ? 15000 : undefined;
|
||||
const result = await client.callTool(tool.name, params as Record<string, unknown>, timeoutMs);
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Commander error: ${err.message}` }],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Lifecycle events
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
// Fire-and-forget probe — don't block session_start chain
|
||||
// (other extensions like footer.ts must not wait for this)
|
||||
probeCommander(ctx).catch(() => {});
|
||||
});
|
||||
|
||||
async function probeCommander(ctx: any) {
|
||||
try {
|
||||
await client.connect();
|
||||
// Lightweight probe — 3s timeout
|
||||
await client.callTool("commander_session", { operation: "list" }, 3000);
|
||||
g.__piCommanderAvailable = true;
|
||||
g.__piCommanderClient = client;
|
||||
ctx.ui.setStatus("Commander: connected", "commander");
|
||||
|
||||
// Resolve gate — drain any ops queued while we were probing
|
||||
const queued = resolveGate(gate, true);
|
||||
drainGateQueue(queued);
|
||||
drainOnReadyCallbacks();
|
||||
|
||||
// Periodic health check (60s)
|
||||
healthCheckTimer = setInterval(async () => {
|
||||
try {
|
||||
if (!client.isConnected()) {
|
||||
await client.connect();
|
||||
}
|
||||
await client.callTool("commander_session", { operation: "list" }, 3000);
|
||||
if (!g.__piCommanderAvailable) {
|
||||
g.__piCommanderAvailable = true;
|
||||
g.__piCommanderClient = client;
|
||||
ctx.ui.setStatus("Commander: connected", "commander");
|
||||
// Recovery — resolve gate if it was reset during offline
|
||||
if (gate.state !== "available") {
|
||||
const queued = resolveGate(gate, true);
|
||||
drainGateQueue(queued);
|
||||
drainOnReadyCallbacks();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
g.__piCommanderAvailable = false;
|
||||
ctx.ui.setStatus("Commander: offline", "commander");
|
||||
// Reset gate so ops queue again until recovery
|
||||
if (gate.state === "available") {
|
||||
resetGate(gate);
|
||||
}
|
||||
}
|
||||
}, 60_000);
|
||||
} catch {
|
||||
g.__piCommanderAvailable = false;
|
||||
ctx.ui.setStatus("Commander: offline", "commander");
|
||||
resolveGate(gate, false);
|
||||
}
|
||||
}
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
if (healthCheckTimer) {
|
||||
clearInterval(healthCheckTimer);
|
||||
healthCheckTimer = undefined;
|
||||
}
|
||||
g.__piCommanderAvailable = false;
|
||||
resetGate(gate);
|
||||
client.disconnect();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user