pi-messenger 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +244 -0
- package/CHANGELOG.md +418 -0
- package/README.md +394 -0
- package/banner.png +0 -0
- package/config-overlay.ts +172 -0
- package/config.ts +178 -0
- package/crew/agents/crew-docs-scout.md +55 -0
- package/crew/agents/crew-gap-analyst.md +105 -0
- package/crew/agents/crew-github-scout.md +111 -0
- package/crew/agents/crew-interview-generator.md +79 -0
- package/crew/agents/crew-plan-sync.md +64 -0
- package/crew/agents/crew-practice-scout.md +62 -0
- package/crew/agents/crew-repo-scout.md +65 -0
- package/crew/agents/crew-reviewer.md +58 -0
- package/crew/agents/crew-web-scout.md +85 -0
- package/crew/agents/crew-worker.md +95 -0
- package/crew/agents.ts +200 -0
- package/crew/handlers/interview.ts +211 -0
- package/crew/handlers/plan.ts +358 -0
- package/crew/handlers/review.ts +341 -0
- package/crew/handlers/status.ts +257 -0
- package/crew/handlers/sync.ts +232 -0
- package/crew/handlers/task.ts +511 -0
- package/crew/handlers/work.ts +289 -0
- package/crew/id-allocator.ts +44 -0
- package/crew/index.ts +229 -0
- package/crew/state.ts +116 -0
- package/crew/store.ts +480 -0
- package/crew/types.ts +164 -0
- package/crew/utils/artifacts.ts +65 -0
- package/crew/utils/config.ts +104 -0
- package/crew/utils/discover.ts +170 -0
- package/crew/utils/install.ts +373 -0
- package/crew/utils/progress.ts +107 -0
- package/crew/utils/result.ts +16 -0
- package/crew/utils/truncate.ts +79 -0
- package/crew-overlay.ts +259 -0
- package/handlers.ts +799 -0
- package/index.ts +591 -0
- package/lib.ts +232 -0
- package/overlay.ts +687 -0
- package/package.json +20 -0
- package/skills/pi-messenger-crew/SKILL.md +140 -0
- package/store.ts +1068 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Configuration Loading
|
|
3
|
+
*
|
|
4
|
+
* Loads and merges user-level and project-level configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import type { MaxOutputConfig } from "./truncate.js";
|
|
11
|
+
|
|
12
|
+
const USER_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "pi-messenger.json");
|
|
13
|
+
const PROJECT_CONFIG_FILE = "config.json";
|
|
14
|
+
|
|
15
|
+
export interface CrewConfig {
|
|
16
|
+
concurrency: {
|
|
17
|
+
scouts: number;
|
|
18
|
+
workers: number;
|
|
19
|
+
};
|
|
20
|
+
truncation: {
|
|
21
|
+
scouts: MaxOutputConfig;
|
|
22
|
+
workers: MaxOutputConfig;
|
|
23
|
+
reviewers: MaxOutputConfig;
|
|
24
|
+
analysts: MaxOutputConfig;
|
|
25
|
+
};
|
|
26
|
+
artifacts: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
cleanupDays: number;
|
|
29
|
+
};
|
|
30
|
+
memory: { enabled: boolean };
|
|
31
|
+
planSync: { enabled: boolean };
|
|
32
|
+
review: { enabled: boolean; maxIterations: number };
|
|
33
|
+
work: { maxAttemptsPerTask: number; maxWaves: number; stopOnBlock: boolean };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_CONFIG: CrewConfig = {
|
|
37
|
+
concurrency: {
|
|
38
|
+
scouts: 4,
|
|
39
|
+
workers: 2,
|
|
40
|
+
},
|
|
41
|
+
truncation: {
|
|
42
|
+
scouts: { bytes: 51200, lines: 500 },
|
|
43
|
+
workers: { bytes: 204800, lines: 5000 },
|
|
44
|
+
reviewers: { bytes: 102400, lines: 2000 },
|
|
45
|
+
analysts: { bytes: 102400, lines: 2000 },
|
|
46
|
+
},
|
|
47
|
+
artifacts: { enabled: true, cleanupDays: 7 },
|
|
48
|
+
memory: { enabled: false },
|
|
49
|
+
planSync: { enabled: false },
|
|
50
|
+
review: { enabled: true, maxIterations: 3 },
|
|
51
|
+
work: { maxAttemptsPerTask: 5, maxWaves: 50, stopOnBlock: false },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function loadJson(filePath: string): Record<string, unknown> {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function deepMerge<T extends object>(target: T, ...sources: Partial<T>[]): T {
|
|
63
|
+
const result = { ...target };
|
|
64
|
+
for (const source of sources) {
|
|
65
|
+
for (const key of Object.keys(source) as (keyof T)[]) {
|
|
66
|
+
const targetVal = result[key];
|
|
67
|
+
const sourceVal = source[key];
|
|
68
|
+
if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal)) {
|
|
69
|
+
result[key] = deepMerge(targetVal as object, sourceVal as object) as T[keyof T];
|
|
70
|
+
} else if (sourceVal !== undefined) {
|
|
71
|
+
result[key] = sourceVal as T[keyof T];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load crew configuration with priority: defaults <- user <- project
|
|
80
|
+
*/
|
|
81
|
+
export function loadCrewConfig(crewDir: string): CrewConfig {
|
|
82
|
+
// User-level config (from ~/.pi/agent/pi-messenger.json -> crew section)
|
|
83
|
+
const userConfig = loadJson(USER_CONFIG_PATH);
|
|
84
|
+
const userCrewConfig = (userConfig.crew ?? {}) as Partial<CrewConfig>;
|
|
85
|
+
|
|
86
|
+
// Project-level config (from .pi/messenger/crew/config.json)
|
|
87
|
+
const projectConfig = loadJson(path.join(crewDir, PROJECT_CONFIG_FILE)) as Partial<CrewConfig>;
|
|
88
|
+
|
|
89
|
+
// Merge: defaults <- user <- project
|
|
90
|
+
return deepMerge(DEFAULT_CONFIG, userCrewConfig, projectConfig);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get truncation config for a specific role.
|
|
95
|
+
*/
|
|
96
|
+
export function getTruncationForRole(config: CrewConfig, role: string): MaxOutputConfig {
|
|
97
|
+
switch (role) {
|
|
98
|
+
case "scout": return config.truncation.scouts;
|
|
99
|
+
case "worker": return config.truncation.workers;
|
|
100
|
+
case "reviewer": return config.truncation.reviewers;
|
|
101
|
+
case "analyst": return config.truncation.analysts;
|
|
102
|
+
default: return config.truncation.workers;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Agent Discovery
|
|
3
|
+
*
|
|
4
|
+
* Discovers agent definitions from user and project directories,
|
|
5
|
+
* with crew-specific filtering by role.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import type { MaxOutputConfig } from "./truncate.js";
|
|
12
|
+
|
|
13
|
+
export type CrewRole = "scout" | "worker" | "reviewer" | "analyst";
|
|
14
|
+
|
|
15
|
+
export interface CrewAgentConfig {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
tools?: string[];
|
|
19
|
+
model?: string;
|
|
20
|
+
systemPrompt: string;
|
|
21
|
+
source: "user" | "project";
|
|
22
|
+
filePath: string;
|
|
23
|
+
// Crew-specific extensions
|
|
24
|
+
crewRole?: CrewRole;
|
|
25
|
+
maxOutput?: MaxOutputConfig;
|
|
26
|
+
parallel?: boolean;
|
|
27
|
+
retryable?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentDiscoveryResult {
|
|
31
|
+
agents: CrewAgentConfig[];
|
|
32
|
+
projectAgentsDir: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse YAML-like frontmatter from markdown content.
|
|
37
|
+
*/
|
|
38
|
+
function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
|
|
39
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
40
|
+
if (!normalized.startsWith("---")) {
|
|
41
|
+
return { frontmatter: {}, body: normalized };
|
|
42
|
+
}
|
|
43
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
44
|
+
if (endIndex === -1) {
|
|
45
|
+
return { frontmatter: {}, body: normalized };
|
|
46
|
+
}
|
|
47
|
+
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
48
|
+
const body = normalized.slice(endIndex + 4).trim();
|
|
49
|
+
|
|
50
|
+
const frontmatter: Record<string, unknown> = {};
|
|
51
|
+
for (const line of frontmatterBlock.split("\n")) {
|
|
52
|
+
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
53
|
+
if (match) {
|
|
54
|
+
let value: unknown = match[2].trim();
|
|
55
|
+
// Handle quoted strings
|
|
56
|
+
if ((value as string).startsWith('"') || (value as string).startsWith("'")) {
|
|
57
|
+
value = (value as string).slice(1, -1);
|
|
58
|
+
}
|
|
59
|
+
// Handle inline YAML objects (e.g., maxOutput: { bytes: 1024, lines: 500 })
|
|
60
|
+
if ((value as string).startsWith("{") && (value as string).endsWith("}")) {
|
|
61
|
+
try {
|
|
62
|
+
// YAML inline objects don't require quoted keys, but JSON does
|
|
63
|
+
const jsonStr = (value as string).replace(/(\w+):/g, '"$1":');
|
|
64
|
+
value = JSON.parse(jsonStr);
|
|
65
|
+
} catch {
|
|
66
|
+
// Keep as string if parse fails
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Handle booleans
|
|
70
|
+
if (value === "true") value = true;
|
|
71
|
+
if (value === "false") value = false;
|
|
72
|
+
frontmatter[match[1]] = value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { frontmatter, body };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isDirectory(p: string): boolean {
|
|
79
|
+
try {
|
|
80
|
+
return fs.statSync(p).isDirectory();
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findProjectAgentsDir(cwd: string): string | null {
|
|
87
|
+
let currentDir = cwd;
|
|
88
|
+
while (true) {
|
|
89
|
+
const candidate = path.join(currentDir, ".pi", "agents");
|
|
90
|
+
if (isDirectory(candidate)) return candidate;
|
|
91
|
+
const parentDir = path.dirname(currentDir);
|
|
92
|
+
if (parentDir === currentDir) return null;
|
|
93
|
+
currentDir = parentDir;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function loadAgentsFromDir(dir: string, source: "user" | "project"): CrewAgentConfig[] {
|
|
98
|
+
if (!fs.existsSync(dir)) return [];
|
|
99
|
+
const agents: CrewAgentConfig[] = [];
|
|
100
|
+
|
|
101
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
102
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
103
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
104
|
+
|
|
105
|
+
const filePath = path.join(dir, entry.name);
|
|
106
|
+
let content: string;
|
|
107
|
+
try {
|
|
108
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
113
|
+
|
|
114
|
+
if (!frontmatter.name || !frontmatter.description) continue;
|
|
115
|
+
|
|
116
|
+
// Parse tools (comma-separated, filter empty)
|
|
117
|
+
const tools = (frontmatter.tools as string)
|
|
118
|
+
?.split(",")
|
|
119
|
+
.map(t => t.trim())
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
|
|
122
|
+
agents.push({
|
|
123
|
+
name: frontmatter.name as string,
|
|
124
|
+
description: frontmatter.description as string,
|
|
125
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
126
|
+
model: frontmatter.model as string | undefined,
|
|
127
|
+
systemPrompt: body,
|
|
128
|
+
source,
|
|
129
|
+
filePath,
|
|
130
|
+
// Crew extensions
|
|
131
|
+
crewRole: frontmatter.crewRole as CrewRole | undefined,
|
|
132
|
+
maxOutput: frontmatter.maxOutput as MaxOutputConfig | undefined,
|
|
133
|
+
parallel: frontmatter.parallel as boolean | undefined ?? true,
|
|
134
|
+
retryable: frontmatter.retryable as boolean | undefined ?? true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return agents;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Discover all agents from user and/or project directories.
|
|
142
|
+
*/
|
|
143
|
+
export function discoverAgents(cwd: string, scope: "user" | "project" | "both"): AgentDiscoveryResult {
|
|
144
|
+
const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
145
|
+
const projectDir = findProjectAgentsDir(cwd);
|
|
146
|
+
|
|
147
|
+
const userAgents = scope !== "project" ? loadAgentsFromDir(userDir, "user") : [];
|
|
148
|
+
const projectAgents = scope !== "user" && projectDir ? loadAgentsFromDir(projectDir, "project") : [];
|
|
149
|
+
|
|
150
|
+
// Project overrides user (same name = project wins)
|
|
151
|
+
const agentMap = new Map<string, CrewAgentConfig>();
|
|
152
|
+
for (const a of userAgents) agentMap.set(a.name, a);
|
|
153
|
+
for (const a of projectAgents) agentMap.set(a.name, a);
|
|
154
|
+
|
|
155
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir: projectDir };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Discover only crew agents (those with crewRole set).
|
|
160
|
+
*/
|
|
161
|
+
export function discoverCrewAgents(cwd: string): CrewAgentConfig[] {
|
|
162
|
+
return discoverAgents(cwd, "both").agents.filter(a => a.crewRole !== undefined);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get crew agents filtered by role.
|
|
167
|
+
*/
|
|
168
|
+
export function getAgentsByRole(cwd: string, role: CrewRole): CrewAgentConfig[] {
|
|
169
|
+
return discoverCrewAgents(cwd).filter(a => a.crewRole === role);
|
|
170
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew - Agent & Skill Installer
|
|
3
|
+
*
|
|
4
|
+
* Copies crew agent definitions and skills from extension source to user directories.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
|
|
12
|
+
// Resolve paths relative to this file
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Agents: crew/agents/ -> ~/.pi/agent/agents/
|
|
17
|
+
const SOURCE_AGENTS_DIR = path.resolve(__dirname, "..", "agents");
|
|
18
|
+
const TARGET_AGENTS_DIR = path.join(homedir(), ".pi", "agent", "agents");
|
|
19
|
+
|
|
20
|
+
// Skills: skills/ -> ~/.pi/agent/skills/
|
|
21
|
+
const SOURCE_SKILLS_DIR = path.resolve(__dirname, "..", "..", "skills");
|
|
22
|
+
const TARGET_SKILLS_DIR = path.join(homedir(), ".pi", "agent", "skills");
|
|
23
|
+
|
|
24
|
+
// List of crew agents to install
|
|
25
|
+
const CREW_AGENTS = [
|
|
26
|
+
// Scouts (5)
|
|
27
|
+
"crew-repo-scout.md",
|
|
28
|
+
"crew-practice-scout.md",
|
|
29
|
+
"crew-docs-scout.md",
|
|
30
|
+
"crew-web-scout.md",
|
|
31
|
+
"crew-github-scout.md",
|
|
32
|
+
// Analysts (3)
|
|
33
|
+
"crew-gap-analyst.md",
|
|
34
|
+
"crew-interview-generator.md",
|
|
35
|
+
"crew-plan-sync.md",
|
|
36
|
+
// Worker (1)
|
|
37
|
+
"crew-worker.md",
|
|
38
|
+
// Reviewer (1)
|
|
39
|
+
"crew-reviewer.md",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// List of skills to install (directory names)
|
|
43
|
+
const CREW_SKILLS = [
|
|
44
|
+
"pi-messenger-crew",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export interface InstallResult {
|
|
48
|
+
installed: string[];
|
|
49
|
+
updated: string[];
|
|
50
|
+
skipped: string[];
|
|
51
|
+
errors: string[];
|
|
52
|
+
targetDir: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if an agent needs updating by comparing modification times.
|
|
57
|
+
*/
|
|
58
|
+
function needsUpdate(sourcePath: string, targetPath: string): boolean {
|
|
59
|
+
if (!fs.existsSync(targetPath)) return true;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const sourceStat = fs.statSync(sourcePath);
|
|
63
|
+
const targetStat = fs.statSync(targetPath);
|
|
64
|
+
return sourceStat.mtimeMs > targetStat.mtimeMs;
|
|
65
|
+
} catch {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check which agents are missing or need updating.
|
|
72
|
+
*/
|
|
73
|
+
export function checkAgentStatus(): { missing: string[]; outdated: string[]; current: string[] } {
|
|
74
|
+
const missing: string[] = [];
|
|
75
|
+
const outdated: string[] = [];
|
|
76
|
+
const current: string[] = [];
|
|
77
|
+
|
|
78
|
+
for (const agent of CREW_AGENTS) {
|
|
79
|
+
const sourcePath = path.join(SOURCE_AGENTS_DIR, agent);
|
|
80
|
+
const targetPath = path.join(TARGET_AGENTS_DIR, agent);
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(sourcePath)) {
|
|
83
|
+
// Source doesn't exist - skip
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(targetPath)) {
|
|
88
|
+
missing.push(agent);
|
|
89
|
+
} else if (needsUpdate(sourcePath, targetPath)) {
|
|
90
|
+
outdated.push(agent);
|
|
91
|
+
} else {
|
|
92
|
+
current.push(agent);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { missing, outdated, current };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Install or update crew agents.
|
|
101
|
+
*
|
|
102
|
+
* @param force - If true, overwrite even if target is newer
|
|
103
|
+
*/
|
|
104
|
+
export function installAgents(force: boolean = false): InstallResult {
|
|
105
|
+
const result: InstallResult = {
|
|
106
|
+
installed: [],
|
|
107
|
+
updated: [],
|
|
108
|
+
skipped: [],
|
|
109
|
+
errors: [],
|
|
110
|
+
targetDir: TARGET_AGENTS_DIR,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Ensure target directory exists
|
|
114
|
+
if (!fs.existsSync(TARGET_AGENTS_DIR)) {
|
|
115
|
+
try {
|
|
116
|
+
fs.mkdirSync(TARGET_AGENTS_DIR, { recursive: true });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
result.errors.push(`Failed to create directory: ${TARGET_AGENTS_DIR}`);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const agent of CREW_AGENTS) {
|
|
124
|
+
const sourcePath = path.join(SOURCE_AGENTS_DIR, agent);
|
|
125
|
+
const targetPath = path.join(TARGET_AGENTS_DIR, agent);
|
|
126
|
+
|
|
127
|
+
// Check source exists
|
|
128
|
+
if (!fs.existsSync(sourcePath)) {
|
|
129
|
+
result.errors.push(`Source not found: ${agent}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if we need to copy
|
|
134
|
+
const targetExists = fs.existsSync(targetPath);
|
|
135
|
+
const shouldUpdate = force || needsUpdate(sourcePath, targetPath);
|
|
136
|
+
|
|
137
|
+
if (!shouldUpdate) {
|
|
138
|
+
result.skipped.push(agent);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Copy the file
|
|
143
|
+
try {
|
|
144
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
145
|
+
if (targetExists) {
|
|
146
|
+
result.updated.push(agent);
|
|
147
|
+
} else {
|
|
148
|
+
result.installed.push(agent);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
result.errors.push(`Failed to copy ${agent}: ${err}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Uninstall crew agents (remove from target directory).
|
|
160
|
+
*/
|
|
161
|
+
export function uninstallAgents(): { removed: string[]; notFound: string[]; errors: string[] } {
|
|
162
|
+
const removed: string[] = [];
|
|
163
|
+
const notFound: string[] = [];
|
|
164
|
+
const errors: string[] = [];
|
|
165
|
+
|
|
166
|
+
for (const agent of CREW_AGENTS) {
|
|
167
|
+
const targetPath = path.join(TARGET_AGENTS_DIR, agent);
|
|
168
|
+
|
|
169
|
+
if (!fs.existsSync(targetPath)) {
|
|
170
|
+
notFound.push(agent);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
fs.unlinkSync(targetPath);
|
|
176
|
+
removed.push(agent);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
errors.push(`Failed to remove ${agent}: ${err}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { removed, notFound, errors };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Ensure agents are installed (auto-install if missing).
|
|
187
|
+
* Returns true if all agents are available.
|
|
188
|
+
*/
|
|
189
|
+
export function ensureAgentsInstalled(): boolean {
|
|
190
|
+
const status = checkAgentStatus();
|
|
191
|
+
|
|
192
|
+
if (status.missing.length === 0 && status.outdated.length === 0) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = installAgents();
|
|
197
|
+
return result.errors.length === 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get the source agents directory path.
|
|
202
|
+
*/
|
|
203
|
+
export function getSourceAgentsDir(): string {
|
|
204
|
+
return SOURCE_AGENTS_DIR;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the target agents directory path.
|
|
209
|
+
*/
|
|
210
|
+
export function getTargetAgentsDir(): string {
|
|
211
|
+
return TARGET_AGENTS_DIR;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// Skills Installation
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
218
|
+
export interface SkillInstallResult {
|
|
219
|
+
installed: string[];
|
|
220
|
+
updated: string[];
|
|
221
|
+
skipped: string[];
|
|
222
|
+
errors: string[];
|
|
223
|
+
targetDir: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if a skill directory needs updating.
|
|
228
|
+
*/
|
|
229
|
+
function skillNeedsUpdate(sourceDir: string, targetDir: string): boolean {
|
|
230
|
+
if (!fs.existsSync(targetDir)) return true;
|
|
231
|
+
|
|
232
|
+
const skillFile = path.join(sourceDir, "SKILL.md");
|
|
233
|
+
const targetFile = path.join(targetDir, "SKILL.md");
|
|
234
|
+
|
|
235
|
+
return needsUpdate(skillFile, targetFile);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check which skills are missing or need updating.
|
|
240
|
+
*/
|
|
241
|
+
export function checkSkillStatus(): { missing: string[]; outdated: string[]; current: string[] } {
|
|
242
|
+
const missing: string[] = [];
|
|
243
|
+
const outdated: string[] = [];
|
|
244
|
+
const current: string[] = [];
|
|
245
|
+
|
|
246
|
+
for (const skill of CREW_SKILLS) {
|
|
247
|
+
const sourceDir = path.join(SOURCE_SKILLS_DIR, skill);
|
|
248
|
+
const targetDir = path.join(TARGET_SKILLS_DIR, skill);
|
|
249
|
+
|
|
250
|
+
if (!fs.existsSync(sourceDir)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!fs.existsSync(targetDir)) {
|
|
255
|
+
missing.push(skill);
|
|
256
|
+
} else if (skillNeedsUpdate(sourceDir, targetDir)) {
|
|
257
|
+
outdated.push(skill);
|
|
258
|
+
} else {
|
|
259
|
+
current.push(skill);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { missing, outdated, current };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Install or update skills.
|
|
268
|
+
*/
|
|
269
|
+
export function installSkills(force: boolean = false): SkillInstallResult {
|
|
270
|
+
const result: SkillInstallResult = {
|
|
271
|
+
installed: [],
|
|
272
|
+
updated: [],
|
|
273
|
+
skipped: [],
|
|
274
|
+
errors: [],
|
|
275
|
+
targetDir: TARGET_SKILLS_DIR,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Ensure target directory exists
|
|
279
|
+
if (!fs.existsSync(TARGET_SKILLS_DIR)) {
|
|
280
|
+
try {
|
|
281
|
+
fs.mkdirSync(TARGET_SKILLS_DIR, { recursive: true });
|
|
282
|
+
} catch (err) {
|
|
283
|
+
result.errors.push(`Failed to create directory: ${TARGET_SKILLS_DIR}`);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const skill of CREW_SKILLS) {
|
|
289
|
+
const sourceDir = path.join(SOURCE_SKILLS_DIR, skill);
|
|
290
|
+
const targetDir = path.join(TARGET_SKILLS_DIR, skill);
|
|
291
|
+
|
|
292
|
+
if (!fs.existsSync(sourceDir)) {
|
|
293
|
+
result.errors.push(`Source not found: ${skill}`);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const targetExists = fs.existsSync(targetDir);
|
|
298
|
+
const shouldUpdate = force || skillNeedsUpdate(sourceDir, targetDir);
|
|
299
|
+
|
|
300
|
+
if (!shouldUpdate) {
|
|
301
|
+
result.skipped.push(skill);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Create target directory
|
|
307
|
+
if (!targetExists) {
|
|
308
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Copy all files from skill directory
|
|
312
|
+
const files = fs.readdirSync(sourceDir);
|
|
313
|
+
for (const file of files) {
|
|
314
|
+
const srcFile = path.join(sourceDir, file);
|
|
315
|
+
const dstFile = path.join(targetDir, file);
|
|
316
|
+
if (fs.statSync(srcFile).isFile()) {
|
|
317
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (targetExists) {
|
|
322
|
+
result.updated.push(skill);
|
|
323
|
+
} else {
|
|
324
|
+
result.installed.push(skill);
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
result.errors.push(`Failed to copy ${skill}: ${err}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Uninstall skills.
|
|
336
|
+
*/
|
|
337
|
+
export function uninstallSkills(): { removed: string[]; notFound: string[]; errors: string[] } {
|
|
338
|
+
const removed: string[] = [];
|
|
339
|
+
const notFound: string[] = [];
|
|
340
|
+
const errors: string[] = [];
|
|
341
|
+
|
|
342
|
+
for (const skill of CREW_SKILLS) {
|
|
343
|
+
const targetDir = path.join(TARGET_SKILLS_DIR, skill);
|
|
344
|
+
|
|
345
|
+
if (!fs.existsSync(targetDir)) {
|
|
346
|
+
notFound.push(skill);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
fs.rmSync(targetDir, { recursive: true });
|
|
352
|
+
removed.push(skill);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
errors.push(`Failed to remove ${skill}: ${err}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return { removed, notFound, errors };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Ensure skills are installed (auto-install if missing).
|
|
363
|
+
*/
|
|
364
|
+
export function ensureSkillsInstalled(): boolean {
|
|
365
|
+
const status = checkSkillStatus();
|
|
366
|
+
|
|
367
|
+
if (status.missing.length === 0 && status.outdated.length === 0) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = installSkills();
|
|
372
|
+
return result.errors.length === 0;
|
|
373
|
+
}
|