Add 5 pi extensions: pi-subagents, pi-crew, rpiv-pi, pi-interactive-shell, pi-intercom

This commit is contained in:
2026-05-08 15:59:25 +10:00
parent d0d1d9b045
commit 31b4110c87
457 changed files with 85157 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { ResourceSource } from "../agents/agent-config.ts";
import type { TeamConfig, TeamRole } from "./team-config.ts";
import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
import { parseGitUrl } from "../utils/git.ts";
import { packageRoot, projectCrewRoot, userPiRoot } from "../utils/paths.ts";
export interface TeamDiscoveryResult {
builtin: TeamConfig[];
user: TeamConfig[];
project: TeamConfig[];
}
function parseRoleSkills(value: string | undefined): string[] | false | undefined {
if (!value) return undefined;
if (value === "false") return false;
const skills = value.split(",").map((entry) => entry.trim()).filter(Boolean);
return skills.length ? skills : undefined;
}
function parseRoleLine(line: string): TeamRole | undefined {
const trimmed = line.trim();
if (!trimmed.startsWith("-")) return undefined;
const value = trimmed.slice(1).trim();
if (!value) return undefined;
const separator = value.indexOf(":");
const namePart = separator >= 0 ? value.slice(0, separator) : value;
const restPart = separator >= 0 ? value.slice(separator + 1) : "";
const name = namePart.trim();
if (!name) return undefined;
const metadata: Record<string, string> = {};
let descriptionSource = restPart.replace(/\bskills\s*=\s*([\w-]+(?:\s*,\s*[\w-]+)*)/g, (_match, raw: string) => {
metadata.skills = raw.replace(/\s*,\s*/g, ",").trim();
return "";
});
descriptionSource = descriptionSource.replace(/\b(agent|model|maxConcurrency)\s*=\s*(\S+)/g, (_match, key: string, raw: string) => {
metadata[key] = raw.trim();
return "";
});
const description = descriptionSource.replace(/\s+/g, " ").trim() || undefined;
const maxConcurrency = metadata.maxConcurrency ? (() => { const p = Number.parseInt(metadata.maxConcurrency, 10); return p > 0 ? p : undefined; })() : undefined;
return {
name,
agent: metadata.agent ?? name,
description,
model: metadata.model,
skills: parseRoleSkills(metadata.skills),
maxConcurrency: maxConcurrency && maxConcurrency > 0 ? maxConcurrency : undefined,
};
}
function parseCost(value: string | undefined): "free" | "cheap" | "expensive" | undefined {
return value === "free" || value === "cheap" || value === "expensive" ? value : undefined;
}
function parseTeamSource(rawSource: string | undefined, fallback: ResourceSource): { source: ResourceSource; sourceUrl: string | undefined } {
if (!rawSource) return { source: fallback, sourceUrl: undefined };
const parsed = parseGitUrl(rawSource);
if (!parsed) return { source: fallback, sourceUrl: undefined };
return { source: "git", sourceUrl: parsed.repo };
}
function parseTeamFile(filePath: string, source: ResourceSource): TeamConfig | undefined {
try {
const content = fs.readFileSync(filePath, "utf-8");
const { frontmatter, body } = parseFrontmatter(content);
const name = frontmatter.name?.trim() || path.basename(filePath, ".team.md");
const roles = body.split("\n").map(parseRoleLine).filter((role): role is TeamRole => role !== undefined);
const triggers = parseCsv(frontmatter.triggers ?? frontmatter.trigger);
const useWhen = parseCsv(frontmatter.useWhen);
const avoidWhen = parseCsv(frontmatter.avoidWhen);
const cost = parseCost(frontmatter.cost);
const category = frontmatter.category?.trim() || undefined;
const sourceInfo = parseTeamSource(frontmatter.source, source);
return {
name,
description: frontmatter.description?.trim() || "No description provided.",
source: sourceInfo.source,
sourceUrl: sourceInfo.sourceUrl,
filePath,
roles,
defaultWorkflow: frontmatter.defaultWorkflow || frontmatter.workflow || undefined,
workspaceMode: frontmatter.workspaceMode?.trim() === "worktree" ? "worktree" : "single",
maxConcurrency: frontmatter.maxConcurrency ? Number.parseInt(frontmatter.maxConcurrency, 10) : undefined,
routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
};
} catch {
return undefined;
}
}
function readTeamDir(dir: string, source: ResourceSource): TeamConfig[] {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter((entry) => entry.endsWith(".team.md"))
.map((entry) => parseTeamFile(path.join(dir, entry), source))
.filter((team): team is TeamConfig => team !== undefined)
.sort((a, b) => a.name.localeCompare(b.name));
}
export function discoverTeams(cwd: string): TeamDiscoveryResult {
return {
builtin: readTeamDir(path.join(packageRoot(), "teams"), "builtin"),
user: readTeamDir(path.join(userPiRoot(), "teams"), "user"),
project: readTeamDir(path.join(projectCrewRoot(cwd), "teams"), "project"),
};
}
export function allTeams(discovery: TeamDiscoveryResult): TeamConfig[] {
const byName = new Map<string, TeamConfig>();
for (const team of [...discovery.project, ...discovery.builtin, ...discovery.user]) {
byName.set(team.name, team);
}
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
}

View File

@@ -0,0 +1,27 @@
import type { ResourceSource, RoutingMetadata } from "../agents/agent-config.ts";
export interface TeamRole {
name: string;
agent: string;
description?: string;
model?: string;
/** Additional skills for this role; false disables role-default injected skills for tasks using this role. */
skills?: string[] | false;
maxConcurrency?: number;
}
export interface TeamConfig {
name: string;
description: string;
source: ResourceSource;
filePath: string;
roles: TeamRole[];
defaultWorkflow?: string;
workspaceMode?: "single" | "worktree";
maxConcurrency?: number;
routing?: RoutingMetadata;
/**
* Optional git-based source URL when this team config is sourced from a remote URL.
*/
sourceUrl?: string;
}

View File

@@ -0,0 +1,38 @@
import type { TeamConfig, TeamRole } from "./team-config.ts";
function line(key: string, value: string | string[] | undefined): string | undefined {
if (value === undefined) return undefined;
if (Array.isArray(value)) return `${key}: ${value.join(", ")}`;
return `${key}: ${value}`;
}
function serializeRole(role: TeamRole): string {
const parts = [`agent=${role.agent}`];
if (role.model) parts.push(`model=${role.model}`);
if (role.skills === false) parts.push("skills=false");
else if (role.skills?.length) parts.push(`skills=${role.skills.join(",")}`);
if (role.maxConcurrency !== undefined) parts.push(`maxConcurrency=${role.maxConcurrency}`);
if (role.description) parts.push(role.description);
return `- ${role.name}: ${parts.join(" ")}`;
}
export function serializeTeam(team: TeamConfig): string {
const lines = [
"---",
`name: ${team.name}`,
`description: ${team.description}`,
team.defaultWorkflow ? `defaultWorkflow: ${team.defaultWorkflow}` : undefined,
team.workspaceMode ? `workspaceMode: ${team.workspaceMode}` : undefined,
team.maxConcurrency !== undefined ? `maxConcurrency: ${team.maxConcurrency}` : undefined,
line("triggers", team.routing?.triggers),
line("useWhen", team.routing?.useWhen),
line("avoidWhen", team.routing?.avoidWhen),
line("cost", team.routing?.cost),
line("category", team.routing?.category),
"---",
"",
...team.roles.map(serializeRole),
"",
].filter((entry): entry is string => entry !== undefined);
return lines.join("\n");
}