feat: add BMAD Copilot extension for VS Code
- Implemented core functionality for integrating BMAD-METHOD agents and workflows into GitHub Copilot Chat. - Added dynamic command discovery for agents and workflows. - Created commands for listing, running, and diagnosing BMAD installations. - Developed a chat handler to process user commands and provide responses. - Introduced a logger for tracking extension activity and errors. - Set up TypeScript configuration and project structure for development. - Included README documentation for usage instructions and development setup.
This commit is contained in:
parent
a8cda7c6fa
commit
9f7a4e4535
|
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
out/
|
||||
*.vsix
|
||||
.vscode-test/
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||
"preLaunchTask": "npm: watch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
# BMAD Copilot
|
||||
|
||||
> BMAD-METHOD integration for VS Code GitHub Copilot Chat.
|
||||
|
||||
Brings the full BMAD agent and workflow experience into Copilot Chat via the `@bmad` participant. Commands, agents, and workflows are discovered dynamically from your BMAD installation — no manual configuration required.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `@bmad /help` | Show available commands and installed items |
|
||||
| `@bmad /doctor` | Diagnose BMAD installation status |
|
||||
| `@bmad /list agents` | List all registered agents with metadata |
|
||||
| `@bmad /list workflows` | List all registered workflows with metadata |
|
||||
| `@bmad /run agent <name> "<task>"` | Execute task with agent persona |
|
||||
| `@bmad /run workflow <name> "<task>"` | Execute task in workflow |
|
||||
| `@bmad /run <name> "<task>"` | Auto-resolve: tries agent first, then workflow |
|
||||
| `@bmad /agents` | List agents (shorthand for /list agents) |
|
||||
| `@bmad /agents <name> "<task>"` | Run agent (shorthand for /run agent) |
|
||||
| `@bmad /workflows` | List workflows (shorthand for /list workflows) |
|
||||
| `@bmad /workflows <name> "<task>"` | Run workflow (shorthand for /run workflow) |
|
||||
|
||||
### Alias & Shorthand Patterns
|
||||
|
||||
The parser recognizes multiple forms to match Cloud Code BMAD experience:
|
||||
|
||||
```
|
||||
@bmad /run agent analyst "Generate PRD" # explicit agent
|
||||
@bmad /run workflow create-prd "Build PRD" # explicit workflow
|
||||
@bmad /run analyst "Generate PRD" # auto-resolve (agent match)
|
||||
@bmad /run create-prd "Build PRD" # auto-resolve (workflow match)
|
||||
@bmad /run a analyst "Generate PRD" # shorthand 'a' for agent
|
||||
@bmad /run w create-prd "Build PRD" # shorthand 'w' for workflow
|
||||
@bmad /agents analyst "Generate PRD" # direct agent command
|
||||
@bmad /workflows create-prd "Build PRD" # direct workflow command
|
||||
@bmad /run bmm agents analyst "Generate PRD" # namespaced by module
|
||||
@bmad /run core workflows brainstorming "Go" # namespaced by module
|
||||
```
|
||||
|
||||
## Dynamic Command Discovery
|
||||
|
||||
The extension automatically scans your BMAD installation for agents and workflows:
|
||||
|
||||
1. **Agent files** — any `*.agent.yaml` or `*.agent.md` file under the BMAD root
|
||||
2. **Workflow files** — any `workflow.yaml`, `workflow.md`, or `workflow-*.md`/`workflow-*.yaml` file
|
||||
|
||||
For each file found, the extension extracts:
|
||||
|
||||
- **Name** — from filename or YAML `name:` field
|
||||
- **Title** — from agent `metadata.title` (agents only)
|
||||
- **Description** — from `persona.role` (agents) or `description:` key (workflows)
|
||||
- **Icon** — from `metadata.icon` (agents only)
|
||||
- **Module** — inferred from directory structure (e.g. `bmm`, `core`)
|
||||
|
||||
The index is rebuilt automatically when files change (via FileSystemWatcher).
|
||||
|
||||
### Detection Paths
|
||||
|
||||
The extension searches for BMAD in this order:
|
||||
|
||||
1. `bmad.rootPath` setting (explicit override)
|
||||
2. `_bmad/` in workspace root
|
||||
3. `.bmad-core/` or `_bmad-core/` in workspace root
|
||||
4. `src/` with `bmm/` or `core/` subdirs (BMAD-METHOD repo layout)
|
||||
|
||||
### Copilot Chat Autocomplete
|
||||
|
||||
All registered slash commands (`/help`, `/doctor`, `/list`, `/run`, `/agents`, `/workflows`) appear in the Copilot Chat autocomplete when you type `@bmad /`.
|
||||
|
||||
## Development (F5)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- VS Code 1.93+
|
||||
- GitHub Copilot + Copilot Chat installed and signed in
|
||||
- Node.js 20+
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cd bmad-copilot
|
||||
npm install
|
||||
```
|
||||
|
||||
### Launch Extension Development Host
|
||||
|
||||
1. Open the `bmad-copilot` folder in VS Code
|
||||
2. Press **F5** (or Run > Start Debugging)
|
||||
3. Select **Run Extension** configuration
|
||||
4. In the Extension Development Host window, open a project with BMAD installed
|
||||
|
||||
### Smoke Tests
|
||||
|
||||
#### Workspace without BMAD
|
||||
|
||||
```
|
||||
@bmad /doctor → Shows "BMAD Root: Not found"
|
||||
```
|
||||
|
||||
#### Workspace with BMAD (`_bmad/` present)
|
||||
|
||||
```
|
||||
@bmad /help → Command table + installed items
|
||||
@bmad /doctor → Detected paths, agent/workflow counts
|
||||
@bmad /list agents → Table with name, title, module, path
|
||||
@bmad /list workflows → Table with name, description, module, path
|
||||
@bmad /run agent analyst "Generate PRD outline" → Copilot responds as analyst
|
||||
@bmad /run analyst "Generate PRD outline" → Same (auto-resolved)
|
||||
@bmad /agents pm "Plan sprints" → Copilot responds as PM
|
||||
@bmad /run workflow create-prd "Build requirements" → Copilot runs workflow
|
||||
@bmad /workflows → Lists all workflows
|
||||
```
|
||||
|
||||
#### BMAD-METHOD repo itself
|
||||
|
||||
The extension detects the source layout (`src/bmm/`, `src/core/`) and indexes agents and workflows from `src/`.
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `bmad.rootPath` | `""` | Explicit BMAD root directory path |
|
||||
| `bmad.autoDetect` | `true` | Auto-detect BMAD installation in workspace |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── extension.ts # Entry point: activation, participant registration
|
||||
├── chatHandler.ts # Chat request handler: routes all commands
|
||||
├── bmadIndex.ts # BMAD directory detection, metadata extraction, indexing
|
||||
├── commandParser.ts # Token parser, alias resolution, fuzzy matching
|
||||
└── logger.ts # OutputChannel logging
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- The VS Code Language Model API requires Copilot Chat authorization. If unavailable, the extension falls back to a copyable assembled prompt with a copy button.
|
||||
- This extension only reads BMAD files. It does not modify any BMAD content.
|
||||
- Workflow files using `.xml` format (e.g. `advanced-elicitation`) are not currently indexed.
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"name": "bmad-copilot",
|
||||
"displayName": "BMAD Copilot",
|
||||
"description": "Integrate BMAD-METHOD agents and workflows into GitHub Copilot Chat via slash commands.",
|
||||
"version": "0.1.0",
|
||||
"publisher": "bmad-code-org",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"vscode": "^1.93.0"
|
||||
},
|
||||
"categories": [
|
||||
"Chat"
|
||||
],
|
||||
"keywords": [
|
||||
"bmad",
|
||||
"copilot",
|
||||
"chat",
|
||||
"agents",
|
||||
"workflows"
|
||||
],
|
||||
"extensionDependencies": [
|
||||
"github.copilot-chat"
|
||||
],
|
||||
"activationEvents": [],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"chatParticipants": [
|
||||
{
|
||||
"id": "bmad-copilot.bmad",
|
||||
"name": "bmad",
|
||||
"fullName": "BMAD Method",
|
||||
"description": "Run BMAD agents and workflows in Copilot Chat. Try /help to get started.",
|
||||
"isSticky": true,
|
||||
"commands": [
|
||||
{
|
||||
"name": "help",
|
||||
"description": "Show available BMAD commands and usage examples"
|
||||
},
|
||||
{
|
||||
"name": "doctor",
|
||||
"description": "Diagnose BMAD installation: workspace path, detected files, errors"
|
||||
},
|
||||
{
|
||||
"name": "list",
|
||||
"description": "List available agents or workflows. Usage: /list agents | /list workflows"
|
||||
},
|
||||
{
|
||||
"name": "run",
|
||||
"description": "Run an agent or workflow. Usage: /run agent <name> | /run workflow <name> | /run <name>"
|
||||
},
|
||||
{
|
||||
"name": "agents",
|
||||
"description": "List agents or run one. Usage: /agents | /agents <name> \"<task>\""
|
||||
},
|
||||
{
|
||||
"name": "workflows",
|
||||
"description": "List workflows or run one. Usage: /workflows | /workflows <name> \"<task>\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "BMAD Copilot",
|
||||
"properties": {
|
||||
"bmad.rootPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Explicit path to the BMAD root directory (e.g. _bmad). Leave empty for auto-detection."
|
||||
},
|
||||
"bmad.autoDetect": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Automatically detect BMAD installation in the workspace."
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "bmad-copilot.copyToClipboard",
|
||||
"title": "BMAD: Copy Prompt to Clipboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"lint": "eslint src --ext ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.93.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { logInfo, logWarn, logError } from './logger';
|
||||
|
||||
/**
|
||||
* Well-known paths where BMAD is installed after `npx bmad-method install`.
|
||||
*/
|
||||
const CANDIDATE_ROOTS = ['_bmad', '.bmad-core', '_bmad-core'];
|
||||
|
||||
export interface BmadItem {
|
||||
/** Display name derived from filename or YAML metadata (e.g. "analyst") */
|
||||
name: string;
|
||||
/** Absolute file path */
|
||||
filePath: string;
|
||||
/** Relative path from workspace root */
|
||||
relativePath: string;
|
||||
/** Human-readable title extracted from YAML (e.g. "Business Analyst") */
|
||||
title?: string;
|
||||
/** Description extracted from YAML front-matter or top-level key */
|
||||
description?: string;
|
||||
/** Icon emoji extracted from YAML metadata */
|
||||
icon?: string;
|
||||
/** Module the item belongs to (e.g. "bmm", "core") */
|
||||
module?: string;
|
||||
}
|
||||
|
||||
export interface BmadIndex {
|
||||
rootPath: string;
|
||||
agents: BmadItem[];
|
||||
workflows: BmadItem[];
|
||||
}
|
||||
|
||||
// ─── Detection ──────────────────────────────────────────
|
||||
|
||||
function workspaceRoot(): string | undefined {
|
||||
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the BMAD root directory.
|
||||
* Priority: explicit config > auto-detect.
|
||||
*/
|
||||
export function detectBmadRoot(): string | undefined {
|
||||
const wsRoot = workspaceRoot();
|
||||
if (!wsRoot) {
|
||||
logWarn('No workspace folder open.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const config = vscode.workspace.getConfiguration('bmad');
|
||||
const explicit: string = config.get<string>('rootPath', '').trim();
|
||||
|
||||
if (explicit) {
|
||||
const abs = path.isAbsolute(explicit) ? explicit : path.join(wsRoot, explicit);
|
||||
if (fs.existsSync(abs)) {
|
||||
logInfo(`Using explicit bmad.rootPath: ${abs}`);
|
||||
return abs;
|
||||
}
|
||||
logWarn(`Configured bmad.rootPath "${explicit}" not found at ${abs}`);
|
||||
}
|
||||
|
||||
const autoDetect = config.get<boolean>('autoDetect', true);
|
||||
if (!autoDetect) {
|
||||
logInfo('Auto-detect disabled and no explicit rootPath.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const candidate of CANDIDATE_ROOTS) {
|
||||
const p = path.join(wsRoot, candidate);
|
||||
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
||||
logInfo(`Auto-detected BMAD root: ${p}`);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if we're inside the BMAD-METHOD repo itself (src/ layout)
|
||||
const srcPath = path.join(wsRoot, 'src');
|
||||
if (fs.existsSync(path.join(srcPath, 'bmm')) || fs.existsSync(path.join(srcPath, 'core'))) {
|
||||
logInfo(`Detected BMAD-METHOD repo layout at: ${srcPath}`);
|
||||
return srcPath;
|
||||
}
|
||||
|
||||
logWarn('BMAD root not found in workspace.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ─── YAML / front-matter metadata extraction ────────────
|
||||
|
||||
/**
|
||||
* Lightweight extraction of metadata from YAML agent files.
|
||||
* Reads plain text and uses regex — no YAML parser dependency.
|
||||
*/
|
||||
function extractAgentMeta(content: string): { title?: string; description?: string; icon?: string; module?: string; role?: string } {
|
||||
const meta: { title?: string; description?: string; icon?: string; module?: string; role?: string } = {};
|
||||
// metadata.title
|
||||
const titleM = content.match(/^\s+title:\s*["']?(.+?)["']?\s*$/m);
|
||||
if (titleM) { meta.title = titleM[1]; }
|
||||
// metadata.icon
|
||||
const iconM = content.match(/^\s+icon:\s*["']?(.+?)["']?\s*$/m);
|
||||
if (iconM) { meta.icon = iconM[1].trim(); }
|
||||
// metadata.module
|
||||
const modM = content.match(/^\s+module:\s*["']?(.+?)["']?\s*$/m);
|
||||
if (modM) { meta.module = modM[1]; }
|
||||
// persona.role
|
||||
const roleM = content.match(/^\s+role:\s*["']?(.+?)["']?\s*$/m);
|
||||
if (roleM) { meta.role = roleM[1]; }
|
||||
// Use role as description if available
|
||||
if (meta.role) { meta.description = meta.role; }
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract name/description from workflow files.
|
||||
* Supports:
|
||||
* - YAML front-matter in .md files (---\nname: ...\ndescription: ...\n---)
|
||||
* - Top-level YAML keys in .yaml files (name: ...\ndescription: ...)
|
||||
*/
|
||||
function extractWorkflowMeta(content: string): { name?: string; description?: string } {
|
||||
const meta: { name?: string; description?: string } = {};
|
||||
// Try YAML front-matter first
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
const block = fmMatch ? fmMatch[1] : content;
|
||||
const nameM = block.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
||||
if (nameM) { meta.name = nameM[1]; }
|
||||
const descM = block.match(/^description:\s*["']?(.+?)["']?\s*$/m);
|
||||
if (descM) { meta.description = descM[1]; }
|
||||
return meta;
|
||||
}
|
||||
|
||||
// ─── Recursive file walker ──────────────────────────────
|
||||
|
||||
function walkAll(dir: string): string[] {
|
||||
const results: string[] = [];
|
||||
function walk(current: string): void {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.isFile()) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(dir);
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─── Name derivation ────────────────────────────────────
|
||||
|
||||
function isAgentFile(filename: string): boolean {
|
||||
return /\.agent\.(yaml|md)$/i.test(filename);
|
||||
}
|
||||
|
||||
function isWorkflowFile(filename: string): boolean {
|
||||
// Matches: workflow.yaml, workflow.md, workflow-*.md, workflow-*.yaml
|
||||
return /^workflow(-[\w-]+)?\.(yaml|md)$/i.test(filename);
|
||||
}
|
||||
|
||||
function deriveAgentName(filePath: string): string {
|
||||
const base = path.basename(filePath);
|
||||
return base.replace(/\.agent\.(yaml|md)$/i, '');
|
||||
}
|
||||
|
||||
function deriveWorkflowName(filePath: string, content: string): string {
|
||||
// Prefer name from YAML metadata
|
||||
const meta = extractWorkflowMeta(content);
|
||||
if (meta.name) { return meta.name; }
|
||||
// Fallback: strip "workflow-" prefix and extension, or use parent dir name
|
||||
const base = path.basename(filePath);
|
||||
const stripped = base.replace(/\.(yaml|md)$/i, '').replace(/^workflow-?/, '');
|
||||
if (stripped) { return stripped; }
|
||||
return path.basename(path.dirname(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the module name from a file path.
|
||||
* e.g. _bmad/bmm/agents/... -> "bmm", _bmad/core/workflows/... -> "core"
|
||||
*/
|
||||
function inferModule(filePath: string, bmadRoot: string): string | undefined {
|
||||
const rel = path.relative(bmadRoot, filePath).replace(/\\/g, '/');
|
||||
const first = rel.split('/')[0];
|
||||
// Only return known module-like names (not the file itself)
|
||||
if (first && !first.includes('.')) { return first; }
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ─── Build index ────────────────────────────────────────
|
||||
|
||||
export function buildIndex(bmadRoot: string): BmadIndex {
|
||||
const wsRoot = workspaceRoot() ?? bmadRoot;
|
||||
const allFiles = walkAll(bmadRoot);
|
||||
|
||||
const agents: BmadItem[] = [];
|
||||
const workflows: BmadItem[] = [];
|
||||
const seenWorkflows = new Set<string>();
|
||||
|
||||
for (const fp of allFiles) {
|
||||
const filename = path.basename(fp);
|
||||
const relPath = path.relative(wsRoot, fp).replace(/\\/g, '/');
|
||||
const mod = inferModule(fp, bmadRoot);
|
||||
|
||||
if (isAgentFile(filename)) {
|
||||
let content = '';
|
||||
try { content = fs.readFileSync(fp, 'utf-8'); } catch { /* ignore */ }
|
||||
const meta = extractAgentMeta(content);
|
||||
agents.push({
|
||||
name: deriveAgentName(fp),
|
||||
filePath: fp,
|
||||
relativePath: relPath,
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
icon: meta.icon,
|
||||
module: meta.module ?? mod,
|
||||
});
|
||||
} else if (isWorkflowFile(filename)) {
|
||||
let content = '';
|
||||
try { content = fs.readFileSync(fp, 'utf-8'); } catch { /* ignore */ }
|
||||
const wfName = deriveWorkflowName(fp, content);
|
||||
const meta = extractWorkflowMeta(content);
|
||||
// Deduplicate by name (keep first found)
|
||||
if (!seenWorkflows.has(wfName.toLowerCase())) {
|
||||
seenWorkflows.add(wfName.toLowerCase());
|
||||
workflows.push({
|
||||
name: wfName,
|
||||
filePath: fp,
|
||||
relativePath: relPath,
|
||||
description: meta.description,
|
||||
module: mod,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
agents.sort((a, b) => a.name.localeCompare(b.name));
|
||||
workflows.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
logInfo(`Index built — ${agents.length} agents, ${workflows.length} workflows`);
|
||||
return { rootPath: bmadRoot, agents, workflows };
|
||||
}
|
||||
|
||||
// ─── State management ───────────────────────────────────
|
||||
|
||||
let _index: BmadIndex | undefined;
|
||||
let _watcher: vscode.FileSystemWatcher | undefined;
|
||||
|
||||
export function getIndex(): BmadIndex | undefined {
|
||||
return _index;
|
||||
}
|
||||
|
||||
export function refreshIndex(): BmadIndex | undefined {
|
||||
const root = detectBmadRoot();
|
||||
if (!root) {
|
||||
_index = undefined;
|
||||
return undefined;
|
||||
}
|
||||
_index = buildIndex(root);
|
||||
return _index;
|
||||
}
|
||||
|
||||
export function startWatching(ctx: vscode.ExtensionContext): void {
|
||||
const wsRoot = workspaceRoot();
|
||||
if (!wsRoot) { return; }
|
||||
|
||||
// Watch for changes in yaml/md files that could be agents or workflows
|
||||
const pattern = new vscode.RelativePattern(wsRoot, '**/*.{yaml,md}');
|
||||
_watcher = vscode.workspace.createFileSystemWatcher(pattern);
|
||||
|
||||
const rebuild = () => {
|
||||
logInfo('File change detected — rebuilding index');
|
||||
refreshIndex();
|
||||
};
|
||||
|
||||
_watcher.onDidCreate(rebuild);
|
||||
_watcher.onDidDelete(rebuild);
|
||||
_watcher.onDidChange(rebuild);
|
||||
ctx.subscriptions.push(_watcher);
|
||||
}
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { getIndex, refreshIndex, BmadIndex, BmadItem } from './bmadIndex';
|
||||
import { parseArgs, parsePromptAsCommand, resolveRunTarget, resolveDirectKind, findClosestName, ResolvedRunTarget } from './commandParser';
|
||||
import { logInfo, logWarn, logError } from './logger';
|
||||
|
||||
// ─── Dynamic help builder ───────────────────────────────
|
||||
|
||||
function buildHelpText(idx: BmadIndex | undefined): string {
|
||||
let md = `## BMAD Copilot — Available Commands\n\n`;
|
||||
md += `| Command | Description |\n`;
|
||||
md += `|---------|-------------|\n`;
|
||||
md += `| \`@bmad /help\` | Show this help |\n`;
|
||||
md += `| \`@bmad /doctor\` | Diagnose BMAD installation |\n`;
|
||||
md += `| \`@bmad /list agents\` | List all registered agents |\n`;
|
||||
md += `| \`@bmad /list workflows\` | List all registered workflows |\n`;
|
||||
md += `| \`@bmad /run agent <name> "<task>"\` | Run task with agent persona |\n`;
|
||||
md += `| \`@bmad /run workflow <name> "<task>"\` | Run task in workflow |\n`;
|
||||
md += `| \`@bmad /run <name> "<task>"\` | Auto-resolve agent or workflow |\n`;
|
||||
md += `| \`@bmad /agents <name> "<task>"\` | Shorthand for /run agent |\n`;
|
||||
md += `| \`@bmad /workflows <name> "<task>"\` | Shorthand for /run workflow |\n`;
|
||||
|
||||
if (idx) {
|
||||
md += `\n**Installed agents (${idx.agents.length}):** `;
|
||||
md += idx.agents.map(a => `\`${a.name}\``).join(', ') || '_none_';
|
||||
md += `\n\n**Installed workflows (${idx.workflows.length}):** `;
|
||||
md += idx.workflows.map(w => `\`${w.name}\``).join(', ') || '_none_';
|
||||
}
|
||||
|
||||
md += `\n\n**Examples:**\n`;
|
||||
md += `\`\`\`\n`;
|
||||
md += `@bmad /run agent analyst "Generate a PRD outline"\n`;
|
||||
md += `@bmad /run create-prd "Build product requirements"\n`;
|
||||
md += `@bmad /agents pm "Plan sprint backlog"\n`;
|
||||
md += `@bmad /list agents\n`;
|
||||
md += `@bmad /doctor\n`;
|
||||
md += `\`\`\`\n`;
|
||||
return md;
|
||||
}
|
||||
|
||||
// ─── Fuzzy suggestion ───────────────────────────────────
|
||||
|
||||
const KNOWN_SUBS = ['help', 'doctor', 'list', 'run', 'agents', 'workflows'];
|
||||
|
||||
function suggestCommand(input: string): string {
|
||||
let best = '';
|
||||
let bestScore = 0;
|
||||
for (const cmd of KNOWN_SUBS) {
|
||||
let score = 0;
|
||||
for (let i = 0; i < Math.min(input.length, cmd.length); i++) {
|
||||
if (input[i] === cmd[i]) { score++; }
|
||||
}
|
||||
if (score > bestScore) { bestScore = score; best = cmd; }
|
||||
}
|
||||
return best || 'help';
|
||||
}
|
||||
|
||||
// ─── Sub-command handlers ───────────────────────────────
|
||||
|
||||
function handleHelp(stream: vscode.ChatResponseStream): void {
|
||||
const idx = getIndex() ?? refreshIndex();
|
||||
stream.markdown(buildHelpText(idx));
|
||||
}
|
||||
|
||||
function handleDoctor(stream: vscode.ChatResponseStream): void {
|
||||
const wsRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '(no workspace)';
|
||||
const config = vscode.workspace.getConfiguration('bmad');
|
||||
const explicitRoot = config.get<string>('rootPath', '') || '(not set)';
|
||||
const autoDetect = config.get<boolean>('autoDetect', true);
|
||||
|
||||
const idx = getIndex() ?? refreshIndex();
|
||||
|
||||
let md = `## BMAD Doctor\n\n`;
|
||||
md += `| Item | Value |\n|------|-------|\n`;
|
||||
md += `| Workspace | \`${wsRoot}\` |\n`;
|
||||
md += `| bmad.rootPath | \`${explicitRoot}\` |\n`;
|
||||
md += `| bmad.autoDetect | \`${autoDetect}\` |\n`;
|
||||
|
||||
if (idx) {
|
||||
md += `| BMAD Root | \`${idx.rootPath}\` |\n`;
|
||||
md += `| Agents | ${idx.agents.length} |\n`;
|
||||
md += `| Workflows | ${idx.workflows.length} |\n`;
|
||||
|
||||
if (idx.agents.length > 0) {
|
||||
md += `\n**Agents:** `;
|
||||
md += idx.agents.map(a => {
|
||||
const label = a.icon ? `${a.icon} ${a.name}` : a.name;
|
||||
return a.title ? `\`${label}\` (${a.title})` : `\`${label}\``;
|
||||
}).join(', ');
|
||||
md += `\n`;
|
||||
}
|
||||
if (idx.workflows.length > 0) {
|
||||
md += `\n**Workflows:** `;
|
||||
md += idx.workflows.map(w => `\`${w.name}\``).join(', ');
|
||||
md += `\n`;
|
||||
}
|
||||
} else {
|
||||
md += `| BMAD Root | **Not found** |\n`;
|
||||
md += `\n> BMAD installation not found. Ensure \`_bmad/\` exists in workspace or set \`bmad.rootPath\` in settings.\n`;
|
||||
}
|
||||
|
||||
stream.markdown(md);
|
||||
}
|
||||
|
||||
function handleList(stream: vscode.ChatResponseStream, args: string[]): void {
|
||||
const idx = getIndex() ?? refreshIndex();
|
||||
if (!idx) {
|
||||
stream.markdown('BMAD installation not found. Run `@bmad /doctor` to check configuration.');
|
||||
return;
|
||||
}
|
||||
|
||||
const what = args[0]?.toLowerCase();
|
||||
if (what === 'agents' || what === 'agent') {
|
||||
listItems(stream, 'Agents', idx.agents, true);
|
||||
} else if (what === 'workflows' || what === 'workflow') {
|
||||
listItems(stream, 'Workflows', idx.workflows, false);
|
||||
} else {
|
||||
stream.markdown(`Specify type: \`@bmad /list agents\` or \`@bmad /list workflows\`\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function listItems(stream: vscode.ChatResponseStream, title: string, items: BmadItem[], isAgent: boolean): void {
|
||||
if (items.length === 0) {
|
||||
stream.markdown(`## ${title}\n\n_No items found._\n`);
|
||||
return;
|
||||
}
|
||||
let md = `## ${title} (${items.length})\n\n`;
|
||||
if (isAgent) {
|
||||
md += `| Name | Title | Module | Path |\n|------|-------|--------|------|\n`;
|
||||
for (const it of items) {
|
||||
const icon = it.icon ? `${it.icon} ` : '';
|
||||
md += `| \`${icon}${it.name}\` | ${it.title ?? '-'} | ${it.module ?? '-'} | \`${it.relativePath}\` |\n`;
|
||||
}
|
||||
} else {
|
||||
md += `| Name | Description | Module | Path |\n|------|-------------|--------|------|\n`;
|
||||
for (const it of items) {
|
||||
const desc = it.description ? truncate(it.description, 60) : '-';
|
||||
md += `| \`${it.name}\` | ${desc} | ${it.module ?? '-'} | \`${it.relativePath}\` |\n`;
|
||||
}
|
||||
}
|
||||
stream.markdown(md);
|
||||
}
|
||||
|
||||
function truncate(s: string, max: number): string {
|
||||
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
||||
}
|
||||
|
||||
// ─── Run agent / workflow ───────────────────────────────
|
||||
|
||||
async function executeRun(
|
||||
request: vscode.ChatRequest,
|
||||
stream: vscode.ChatResponseStream,
|
||||
token: vscode.CancellationToken,
|
||||
resolved: ResolvedRunTarget
|
||||
): Promise<void> {
|
||||
const { kind, item, task: userTask } = resolved;
|
||||
|
||||
// Read file content
|
||||
let fileContent: string;
|
||||
try {
|
||||
fileContent = fs.readFileSync(item.filePath, 'utf-8');
|
||||
logInfo(`Read ${kind} file: ${item.filePath} (${fileContent.length} chars)`);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError(`Failed to read ${item.filePath}: ${msg}`);
|
||||
stream.markdown(`Failed to read ${kind} file \`${item.relativePath}\`: ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = userTask || '(no specific task provided — greet the user and describe your capabilities)';
|
||||
|
||||
const systemPrompt = `You are acting as a BMAD ${kind}. Below is the ${kind} definition file content. Follow the persona, instructions, and capabilities described within.\n\n--- BEGIN ${kind.toUpperCase()} DEFINITION ---\n${fileContent}\n--- END ${kind.toUpperCase()} DEFINITION ---`;
|
||||
|
||||
const userMessage = `User task: ${task}`;
|
||||
|
||||
try {
|
||||
const model = request.model;
|
||||
if (model) {
|
||||
const label = item.icon ? `${item.icon} ${item.name}` : item.name;
|
||||
logInfo(`Sending prompt to model: ${model.id}`);
|
||||
stream.progress(`Running as ${label} ${kind}...`);
|
||||
|
||||
const messages = [
|
||||
vscode.LanguageModelChatMessage.User(systemPrompt),
|
||||
vscode.LanguageModelChatMessage.User(userMessage),
|
||||
];
|
||||
|
||||
const chatResponse = await model.sendRequest(messages, {}, token);
|
||||
for await (const fragment of chatResponse.text) {
|
||||
stream.markdown(fragment);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logWarn(`LLM direct call failed (${msg}), falling back to prompt display.`);
|
||||
}
|
||||
|
||||
fallbackPrompt(stream, item, kind, fileContent, task);
|
||||
}
|
||||
|
||||
function handleRun(
|
||||
request: vscode.ChatRequest,
|
||||
stream: vscode.ChatResponseStream,
|
||||
token: vscode.CancellationToken,
|
||||
args: string[]
|
||||
): Promise<void> {
|
||||
const idx = getIndex() ?? refreshIndex();
|
||||
if (!idx) {
|
||||
stream.markdown('BMAD installation not found. Run `@bmad /doctor` to check configuration.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const resolved = resolveRunTarget(args, idx);
|
||||
if (!resolved) {
|
||||
// Could not resolve — provide helpful feedback
|
||||
const name = args[0]?.toLowerCase();
|
||||
if (name) {
|
||||
const suggestion = findClosestName(name, idx);
|
||||
let msg = `Could not resolve \`${args.join(' ')}\`.`;
|
||||
if (suggestion) { msg += ` Did you mean \`${suggestion}\`?`; }
|
||||
msg += `\n\nUsage: \`@bmad /run agent <name> "<task>"\` or \`@bmad /run <name> "<task>"\``;
|
||||
msg += `\n\nRun \`@bmad /list agents\` or \`@bmad /list workflows\` to see available names.`;
|
||||
stream.markdown(msg);
|
||||
} else {
|
||||
stream.markdown('Usage: `@bmad /run agent <name> "<task>"` or `@bmad /run workflow <name> "<task>"`\n\nRun `@bmad /list agents` to see available names.');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return executeRun(request, stream, token, resolved);
|
||||
}
|
||||
|
||||
function fallbackPrompt(
|
||||
stream: vscode.ChatResponseStream,
|
||||
item: BmadItem,
|
||||
kind: string,
|
||||
fileContent: string,
|
||||
task: string
|
||||
): void {
|
||||
stream.markdown(`> LLM API unavailable. Copy the assembled prompt below and paste it into Copilot Chat.\n\n`);
|
||||
|
||||
const assembled = `I want you to adopt the following ${kind} persona and follow its instructions exactly.\n\n--- BEGIN ${kind.toUpperCase()} DEFINITION ---\n${fileContent}\n--- END ${kind.toUpperCase()} DEFINITION ---\n\nNow respond to this task:\n${task}`;
|
||||
|
||||
stream.markdown('```\n' + assembled + '\n```\n');
|
||||
|
||||
stream.button({
|
||||
title: 'Copy Prompt',
|
||||
command: 'bmad-copilot.copyToClipboard',
|
||||
arguments: [assembled],
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Main handler ───────────────────────────────────────
|
||||
|
||||
export const chatHandler: vscode.ChatRequestHandler = async (
|
||||
request: vscode.ChatRequest,
|
||||
context: vscode.ChatContext,
|
||||
stream: vscode.ChatResponseStream,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.ChatResult> => {
|
||||
try {
|
||||
logInfo(`Chat request — command: ${request.command ?? '(none)'}, prompt: "${request.prompt}"`);
|
||||
|
||||
let command = request.command;
|
||||
let args: string[] = [];
|
||||
|
||||
if (command) {
|
||||
args = parseArgs(request.prompt.trim());
|
||||
} else {
|
||||
const parsed = parsePromptAsCommand(request.prompt);
|
||||
if (parsed) {
|
||||
command = parsed.subCommand;
|
||||
args = parsed.args;
|
||||
}
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'help':
|
||||
handleHelp(stream);
|
||||
break;
|
||||
case 'doctor':
|
||||
handleDoctor(stream);
|
||||
break;
|
||||
case 'list':
|
||||
handleList(stream, args);
|
||||
break;
|
||||
case 'run':
|
||||
await handleRun(request, stream, token, args);
|
||||
break;
|
||||
case 'agents': {
|
||||
// Shorthand: @bmad /agents <name> "<task>"
|
||||
const idx = getIndex() ?? refreshIndex();
|
||||
if (!idx) { stream.markdown('BMAD installation not found. Run `@bmad /doctor`.'); break; }
|
||||
const resolved = resolveDirectKind('agent', args, idx);
|
||||
if (resolved) {
|
||||
await executeRun(request, stream, token, resolved);
|
||||
} else {
|
||||
listItems(stream, 'Agents', idx.agents, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'workflows': {
|
||||
// Shorthand: @bmad /workflows <name> "<task>"
|
||||
const idx = getIndex() ?? refreshIndex();
|
||||
if (!idx) { stream.markdown('BMAD installation not found. Run `@bmad /doctor`.'); break; }
|
||||
const resolved = resolveDirectKind('workflow', args, idx);
|
||||
if (resolved) {
|
||||
await executeRun(request, stream, token, resolved);
|
||||
} else {
|
||||
listItems(stream, 'Workflows', idx.workflows, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Try to treat as agent/workflow name directly
|
||||
if (command) {
|
||||
const idx = getIndex() ?? refreshIndex();
|
||||
if (idx) {
|
||||
const resolved = resolveRunTarget([command, ...args], idx);
|
||||
if (resolved) {
|
||||
await executeRun(request, stream, token, resolved);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const suggestion = suggestCommand(command);
|
||||
stream.markdown(`Unknown command \`${command}\`. Did you mean \`@bmad /${suggestion}\`?\n\n`);
|
||||
}
|
||||
handleHelp(stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError(`Unhandled error in chat handler: ${msg}`);
|
||||
stream.markdown(`Error: ${msg}\n\nTry \`@bmad /help\` or \`@bmad /doctor\`.`);
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
import { BmadIndex, BmadItem } from './bmadIndex';
|
||||
|
||||
/**
|
||||
* Parse the user's remaining prompt text after the slash-command into tokens.
|
||||
*
|
||||
* Supports:
|
||||
* - Quoted arguments: "some multi-word arg"
|
||||
* - Unquoted arguments split by whitespace
|
||||
*/
|
||||
export function parseArgs(prompt: string): string[] {
|
||||
const args: string[] = [];
|
||||
let current = '';
|
||||
let inQuote = false;
|
||||
let quoteChar = '';
|
||||
|
||||
for (let i = 0; i < prompt.length; i++) {
|
||||
const ch = prompt[i];
|
||||
if (inQuote) {
|
||||
if (ch === quoteChar) {
|
||||
inQuote = false;
|
||||
args.push(current);
|
||||
current = '';
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
} else if (ch === '"' || ch === "'") {
|
||||
inQuote = true;
|
||||
quoteChar = ch;
|
||||
if (current.length > 0) {
|
||||
args.push(current);
|
||||
current = '';
|
||||
}
|
||||
} else if (/\s/.test(ch)) {
|
||||
if (current.length > 0) {
|
||||
args.push(current);
|
||||
current = '';
|
||||
}
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
if (current.length > 0) {
|
||||
args.push(current);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed result of a user prompt.
|
||||
*/
|
||||
export interface ParsedCommand {
|
||||
subCommand: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse a sub-command from raw prompt text (fallback when no VS Code command is set).
|
||||
*/
|
||||
export function parsePromptAsCommand(prompt: string): ParsedCommand | undefined {
|
||||
const tokens = parseArgs(prompt.trim());
|
||||
if (tokens.length === 0) { return undefined; }
|
||||
return { subCommand: tokens[0].toLowerCase(), args: tokens.slice(1) };
|
||||
}
|
||||
|
||||
// ─── Resolved command ───────────────────────────────────
|
||||
|
||||
export type ResolvedKind = 'agent' | 'workflow';
|
||||
|
||||
export interface ResolvedRunTarget {
|
||||
kind: ResolvedKind;
|
||||
item: BmadItem;
|
||||
task: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to resolve a "run" command's arguments into a concrete agent or workflow.
|
||||
*
|
||||
* Supported patterns (all case-insensitive):
|
||||
* agent <name> "<task>" — explicit agent
|
||||
* workflow <name> "<task>" — explicit workflow
|
||||
* a <name> "<task>" — shorthand agent
|
||||
* w <name> "<task>" — shorthand workflow
|
||||
* <name> "<task>" — auto-resolve: try agent first, then workflow
|
||||
* bmm agents <name> "<task>" — namespaced agent
|
||||
* bmm workflows <name> "<task>" — namespaced workflow
|
||||
* core agents <name> "<task>" — namespaced agent
|
||||
* core workflows <name> "<task>" — namespaced workflow
|
||||
*/
|
||||
export function resolveRunTarget(args: string[], index: BmadIndex): ResolvedRunTarget | undefined {
|
||||
if (args.length === 0) { return undefined; }
|
||||
|
||||
const first = args[0].toLowerCase();
|
||||
|
||||
// ── Explicit kind ──
|
||||
if (first === 'agent' || first === 'a') {
|
||||
return findItem('agent', args.slice(1), index);
|
||||
}
|
||||
if (first === 'workflow' || first === 'w') {
|
||||
return findItem('workflow', args.slice(1), index);
|
||||
}
|
||||
|
||||
// ── Namespaced: bmm/core agents/workflows <name> ──
|
||||
const second = args[1]?.toLowerCase();
|
||||
if (isModuleName(first) && second) {
|
||||
if (second === 'agents' || second === 'agent' || second === 'a') {
|
||||
return findItem('agent', args.slice(2), index, first);
|
||||
}
|
||||
if (second === 'workflows' || second === 'workflow' || second === 'w') {
|
||||
return findItem('workflow', args.slice(2), index, first);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auto-resolve: try as agent name, then workflow name ──
|
||||
const item = findByName(first, index);
|
||||
if (item) {
|
||||
return { kind: item.kind, item: item.item, task: args.slice(1).join(' ') };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve arguments for /agents <name> "<task>" and /workflows <name> "<task>" shorthand commands.
|
||||
*/
|
||||
export function resolveDirectKind(kind: ResolvedKind, args: string[], index: BmadIndex): ResolvedRunTarget | undefined {
|
||||
return findItem(kind, args, index);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
function isModuleName(s: string): boolean {
|
||||
return /^[a-z][a-z0-9_-]*$/.test(s) && !['agent', 'workflow', 'a', 'w', 'agents', 'workflows'].includes(s);
|
||||
}
|
||||
|
||||
function findItem(kind: ResolvedKind, args: string[], index: BmadIndex, moduleFilter?: string): ResolvedRunTarget | undefined {
|
||||
if (args.length === 0) { return undefined; }
|
||||
const name = args[0].toLowerCase();
|
||||
const task = args.slice(1).join(' ');
|
||||
const list = kind === 'agent' ? index.agents : index.workflows;
|
||||
let item = list.find(i => i.name.toLowerCase() === name && (!moduleFilter || i.module === moduleFilter));
|
||||
if (!item) {
|
||||
// Fallback: ignore module filter
|
||||
item = list.find(i => i.name.toLowerCase() === name);
|
||||
}
|
||||
if (!item) { return undefined; }
|
||||
return { kind, item, task };
|
||||
}
|
||||
|
||||
function findByName(name: string, index: BmadIndex): { kind: ResolvedKind; item: BmadItem } | undefined {
|
||||
const n = name.toLowerCase();
|
||||
const agent = index.agents.find(a => a.name.toLowerCase() === n);
|
||||
if (agent) { return { kind: 'agent', item: agent }; }
|
||||
const wf = index.workflows.find(w => w.name.toLowerCase() === n);
|
||||
if (wf) { return { kind: 'workflow', item: wf }; }
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest matching item name across agents and workflows for suggestions.
|
||||
*/
|
||||
export function findClosestName(input: string, index: BmadIndex): string | undefined {
|
||||
const all = [
|
||||
...index.agents.map(a => a.name),
|
||||
...index.workflows.map(w => w.name),
|
||||
];
|
||||
if (all.length === 0) { return undefined; }
|
||||
const lower = input.toLowerCase();
|
||||
// Prefix match first
|
||||
const prefix = all.find(n => n.toLowerCase().startsWith(lower));
|
||||
if (prefix) { return prefix; }
|
||||
// Substring match
|
||||
const sub = all.find(n => n.toLowerCase().includes(lower));
|
||||
if (sub) { return sub; }
|
||||
// Levenshtein-like: best character overlap
|
||||
let best = all[0];
|
||||
let bestScore = 0;
|
||||
for (const n of all) {
|
||||
let score = 0;
|
||||
const nl = n.toLowerCase();
|
||||
for (let i = 0; i < Math.min(lower.length, nl.length); i++) {
|
||||
if (lower[i] === nl[i]) { score++; }
|
||||
}
|
||||
if (score > bestScore) { bestScore = score; best = n; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { initLogger, logInfo } from './logger';
|
||||
import { refreshIndex, startWatching } from './bmadIndex';
|
||||
import { chatHandler } from './chatHandler';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext): void {
|
||||
// ── Logger ──
|
||||
initLogger(context);
|
||||
logInfo('BMAD Copilot extension activating…');
|
||||
|
||||
// ── Initial index ──
|
||||
const idx = refreshIndex();
|
||||
if (idx) {
|
||||
logInfo(`Activated with ${idx.agents.length} agents, ${idx.workflows.length} workflows`);
|
||||
} else {
|
||||
logInfo('No BMAD installation detected in current workspace.');
|
||||
}
|
||||
|
||||
// ── File watcher ──
|
||||
startWatching(context);
|
||||
|
||||
// ── Chat participant ──
|
||||
const participant = vscode.chat.createChatParticipant('bmad-copilot.bmad', chatHandler);
|
||||
participant.iconPath = new vscode.ThemeIcon('rocket');
|
||||
context.subscriptions.push(participant);
|
||||
|
||||
// ── Copy-to-clipboard command (for fallback prompt) ──
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('bmad-copilot.copyToClipboard', async (text: string) => {
|
||||
await vscode.env.clipboard.writeText(text);
|
||||
vscode.window.showInformationMessage('BMAD prompt copied to clipboard!');
|
||||
})
|
||||
);
|
||||
|
||||
// ── Listen for config changes ──
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('bmad')) {
|
||||
logInfo('BMAD config changed — rebuilding index');
|
||||
refreshIndex();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
logInfo('BMAD Copilot extension activated.');
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
// Cleanup handled by context.subscriptions
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
const CHANNEL_NAME = 'BMAD Copilot';
|
||||
let _channel: vscode.OutputChannel | undefined;
|
||||
|
||||
export function initLogger(ctx: vscode.ExtensionContext): vscode.OutputChannel {
|
||||
_channel = vscode.window.createOutputChannel(CHANNEL_NAME);
|
||||
ctx.subscriptions.push(_channel);
|
||||
return _channel;
|
||||
}
|
||||
|
||||
function ts(): string {
|
||||
return new Date().toISOString().slice(11, 23);
|
||||
}
|
||||
|
||||
export function logInfo(msg: string): void {
|
||||
_channel?.appendLine(`[${ts()}] INFO ${msg}`);
|
||||
}
|
||||
|
||||
export function logWarn(msg: string): void {
|
||||
_channel?.appendLine(`[${ts()}] WARN ${msg}`);
|
||||
}
|
||||
|
||||
export function logError(msg: string): void {
|
||||
_channel?.appendLine(`[${ts()}] ERROR ${msg}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "ES2022",
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"lib": ["ES2022"],
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"moduleResolution": "Node16"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "out"]
|
||||
}
|
||||
Loading…
Reference in New Issue