rrce-workflow 0.2.8 → 0.2.10

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/README.md CHANGED
@@ -5,89 +5,198 @@
5
5
  [![npm version](https://badge.fury.io/js/rrce-workflow.svg)](https://www.npmjs.com/package/rrce-workflow)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- RRCE-Workflow is a TUI that helps you set up and manage AI agent workflows for your codebase. It works with GitHub Copilot, Antigravity IDE, and other AI coding tools.
8
+ RRCE-Workflow is a CLI wizard that sets up AI agent prompts and workflows for your codebase. It works with **GitHub Copilot**, **Antigravity IDE**, and other AI coding assistants.
9
9
 
10
10
  ## Installation
11
11
 
12
12
  ```bash
13
- # Using npm
13
+ # Quick start (no install needed)
14
14
  npx rrce-workflow
15
15
 
16
- # Using bun (recommended)
17
- bunx rrce-workflow
18
-
19
- # Global install
16
+ # Or install globally
20
17
  npm install -g rrce-workflow
21
18
  ```
22
19
 
23
- ## Quick Start
20
+ ---
21
+
22
+ ## How to Use
23
+
24
+ ### 1. Initial Setup
25
+
26
+ Run the wizard in your project directory:
24
27
 
25
28
  ```bash
26
- # Run setup wizard
27
- rrce-workflow wizard
29
+ cd your-project
30
+ npx rrce-workflow
31
+ ```
32
+
33
+ The wizard will:
34
+ 1. Ask where to store workflow data (global, workspace, or both)
35
+ 2. Let you choose a custom global path if the default isn't writable
36
+ 3. Ask which AI tools you use (GitHub Copilot, Antigravity)
37
+ 4. Set up prompts and knowledge folders
38
+
39
+ ### 2. Using the Agent Prompts
28
40
 
29
- # Or just run to see available agents
30
- rrce-workflow
41
+ After setup, you'll have agent prompts in IDE-specific folders:
42
+
43
+ - **GitHub Copilot**: `.github/agents/*.agent.md`
44
+ - **Antigravity**: `.agent/workflows/*.md`
45
+
46
+ In your AI assistant, invoke prompts using their names:
47
+
48
+ | Agent | Invoke With | What It Does |
49
+ |-------|-------------|--------------|
50
+ | **Init** | `/init` | Analyze your codebase and create `project-context.md` |
51
+ | **Research** | `/research REQUEST="..." TASK_SLUG=my-task` | Clarify requirements, create research brief |
52
+ | **Planning** | `/plan TASK_SLUG=my-task` | Create actionable execution plan |
53
+ | **Execute** | `/execute TASK_SLUG=my-task` | Implement the planned work |
54
+ | **Docs** | `/docs DOC_TYPE=architecture` | Generate documentation |
55
+ | **Sync** | `/sync` | Update knowledge base after code changes |
56
+
57
+ ### 3. Recommended Workflow (RRCE Pipeline)
58
+
59
+ ```
60
+ 1. /init → Establish project context
61
+ 2. /research → Clarify requirements for a new task
62
+ 3. /plan → Create execution plan
63
+ 4. /execute → Implement the plan
64
+ 5. /docs → Generate documentation (optional)
65
+ 6. /sync → Keep knowledge base current (periodic)
31
66
  ```
32
67
 
33
- ## Features
68
+ #### Pipeline Stages Explained
34
69
 
35
- - **Setup Wizard** - Interactive configuration for storage mode and AI tools
36
- - **Agent Prompts** - Pre-built prompts for init, research, planning, execution, documentation, and sync
37
- - **Multi-Tool Support** - Works with GitHub Copilot (`.agent.md`) and Antigravity IDE
38
- - **Cross-Project References** - Reference context from related projects
70
+ **🔍 Init** Scans your codebase to understand tech stack, architecture, coding conventions, and project structure. Creates `project-context.md` that all other agents rely on. Run once at project start, and again when major changes occur.
39
71
 
40
- ## Agents
72
+ **💬 Research** — Entry point for new tasks. Takes a user request and engages in clarifying discussion to refine scope, surface risks, and identify gaps. Produces a research brief for the Planning agent.
41
73
 
42
- | Agent | Command | Description |
43
- |-------|---------|-------------|
44
- | **Init** | `/init` | Initialize or update project context |
45
- | **Research** | `/research` | Clarify requirements and create research brief |
46
- | **Planning** | `/plan` | Transform requirements into execution plan |
47
- | **Executor** | `/execute` | Implement the planned tasks |
48
- | **Documentation** | `/docs` | Generate project documentation |
49
- | **Sync** | `/sync` | Reconcile codebase with knowledge base |
74
+ **📋 Planning** Transforms the research brief into an ordered, actionable execution plan. Breaks work into tasks with dependencies, acceptance criteria, and testing strategy. Ensures the Executor has clear guidance.
50
75
 
51
- ## Configuration
76
+ **⚡ Execute** — Implements the planned work. Writes code, adds tests, runs verifications. Updates task metadata and logs execution notes for auditability.
52
77
 
53
- After running the wizard, a `.rrce-workflow.yaml` is created in your project:
78
+ **📄 Docs** — Synthesizes the completed work into documentation. Can generate API docs, architecture overviews, runbooks, or changelogs based on `DOC_TYPE`.
54
79
 
55
- ```yaml
56
- version: 1
80
+ **🔄 Sync** — Maintenance agent that reconciles the knowledge base with actual code. Run periodically to catch drift and keep documentation accurate.
57
81
 
82
+ ---
83
+
84
+ ## How It Works
85
+
86
+ ### Path Resolution
87
+
88
+ All agents read `.rrce-workflow/config.yaml` to resolve paths:
89
+
90
+ ```yaml
58
91
  storage:
59
- mode: global # or: workspace, both
92
+ mode: workspace # or: global, both
93
+ globalPath: "~/.rrce-workflow" # optional custom path
60
94
 
61
95
  project:
62
96
  name: "my-project"
63
97
  ```
64
98
 
65
- ### Storage Modes
99
+ Agents resolve `{{RRCE_DATA}}` based on storage mode:
100
+ - `workspace` → `.rrce-workflow/`
101
+ - `global` → `~/.rrce-workflow/workspaces/my-project/`
102
+ - `both` → `.rrce-workflow/` (primary, synced to global)
66
103
 
67
- | Mode | Location | Use Case |
68
- |------|----------|----------|
69
- | `global` | `~/.rrce-workflow/workspaces/<name>/` | Non-intrusive, cross-project access |
70
- | `workspace` | `.rrce-workflow/` | Portable with repo, team sharing |
71
- | `both` | Both locations (synced) | Redundancy + cross-project access |
104
+ ### Cross-Project References
72
105
 
73
- When `both` is selected, data is stored in **both** locations simultaneously:
74
- - Primary (for reads): `<workspace>/.rrce-workflow/`
75
- - Secondary (auto-synced): `~/.rrce-workflow/workspaces/<name>/`
106
+ When using `global` or `both` mode, you can reference other projects:
76
107
 
77
- Each storage location contains:
78
108
  ```
79
- <storage-path>/
80
- ├── knowledge/ # Project context and domain knowledge
81
- ├── prompts/ # Agent prompt files
82
- ├── refs/ # External references
83
- ├── tasks/ # Task artifacts and metadata
84
- └── templates/ # Output templates
109
+ ~/.rrce-workflow/workspaces/other-project/knowledge/project-context.md
110
+ ```
111
+
112
+ This enables a frontend app to reference its backend API's knowledge!
113
+
114
+ ---
115
+
116
+ ## Folder Structure
117
+
118
+ After setup, your project will have:
119
+
120
+ ```
121
+ your-project/
122
+ ├── .rrce-workflow/ # Data storage
123
+ │ ├── config.yaml # Configuration
124
+ │ ├── knowledge/ # Project context
125
+ │ ├── refs/ # External references
126
+ │ ├── tasks/ # Task artifacts by slug
127
+ │ └── templates/ # Output templates
128
+ ├── .github/agents/ # GitHub Copilot prompts
129
+ │ └── *.agent.md
130
+ └── .agent/workflows/ # Antigravity prompts
131
+ └── *.md
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Wizard Options
137
+
138
+ When you run the wizard on an already-configured project, you'll see:
139
+
140
+ | Option | Description |
141
+ |--------|-------------|
142
+ | **Link other project knowledge** | Reference knowledge from other projects in global storage |
143
+ | **Sync to global storage** | Copy workspace data to global (enables cross-project access) |
144
+ | **Update from package** | Get latest prompts and templates |
145
+
146
+ ---
147
+
148
+ ## Storage Mode Comparison
149
+
150
+ | Mode | Location | Best For |
151
+ |------|----------|----------|
152
+ | `global` | `~/.rrce-workflow/workspaces/<name>/` | Clean workspace, cross-project references |
153
+ | `workspace` | `.rrce-workflow/` | Team sharing, portable with repo |
154
+ | `both` | Both locations (synced) | Full redundancy + cross-project access |
155
+
156
+ ---
157
+
158
+ ## Custom Global Path
159
+
160
+ If the default `~/.rrce-workflow` isn't writable (common with `npx` in enterprise environments), the wizard lets you choose a custom location:
161
+
162
+ ```yaml
163
+ storage:
164
+ mode: global
165
+ globalPath: "/path/to/custom/rrce-workflow"
85
166
  ```
86
167
 
168
+ ---
169
+
87
170
  ## Requirements
88
171
 
89
- - Node.js 18+ or Bun 1.0+
90
- - Git (for user detection)
172
+ - **Node.js 18+**
173
+ - **Git** (for user detection)
174
+
175
+ ---
176
+
177
+ ## Troubleshooting
178
+
179
+ ### "Permission denied" when setting up
180
+
181
+ If you can't write to `~/.rrce-workflow`, the wizard will prompt you to choose a custom path. You can also set it manually:
182
+
183
+ ```bash
184
+ export RRCE_HOME=/path/to/writable/location
185
+ npx rrce-workflow
186
+ ```
187
+
188
+ ### Agents can't find data files
189
+
190
+ Make sure the agent reads `.rrce-workflow/config.yaml` first. All prompts include a mandatory first step to resolve paths from the config.
191
+
192
+ ### Updating prompts after package update
193
+
194
+ ```bash
195
+ npx rrce-workflow
196
+ # Select "Update from package"
197
+ ```
198
+
199
+ ---
91
200
 
92
201
  ## License
93
202
 
@@ -20,6 +20,14 @@ auto-identity:
20
20
 
21
21
  You are the Documentation Lead for the project. Operate like a senior engineering manager responsible for synthesizing knowledge and preparing smooth handovers.
22
22
 
23
+ **⚠️ FIRST STEP (MANDATORY)**
24
+ Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
25
+ ```
26
+ RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
27
+ RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
28
+ ```
29
+ Use these resolved paths for ALL subsequent file operations.
30
+
23
31
  Pipeline Position
24
32
  - **Optional**: Documentation can be run at any point, but is most valuable after Execution.
25
33
  - **Best After**: Executor phase complete (if documenting a specific task).
@@ -16,6 +16,14 @@ auto-identity:
16
16
 
17
17
  You are the Executor for the project. Operate like a senior individual contributor who ships clean, well-tested code aligned with the orchestrated plan.
18
18
 
19
+ **⚠️ FIRST STEP (MANDATORY)**
20
+ Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
21
+ ```
22
+ RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
23
+ RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
24
+ ```
25
+ Use these resolved paths for ALL subsequent file operations.
26
+
19
27
  Pipeline Position
20
28
  - **Requires**: Planning phase must be complete before execution can begin.
21
29
  - **Next Step**: After execution is complete, optionally hand off to `/docs` (Documentation agent).
@@ -15,6 +15,14 @@ auto-identity:
15
15
 
16
16
  You are the Project Initializer for RRCE-Workflow. Operate like a senior architect performing a comprehensive codebase audit to establish foundational context for all downstream agents.
17
17
 
18
+ **⚠️ FIRST STEP (MANDATORY)**
19
+ Before doing ANY work, read `.rrce-workflow/config.yaml` (if it exists) and resolve these variables:
20
+ ```
21
+ RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
22
+ RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
23
+ ```
24
+ If config doesn't exist yet (new project), use defaults: `RRCE_HOME=~/.rrce-workflow`, `RRCE_DATA=.rrce-workflow/`
25
+
18
26
  Pipeline Position
19
27
  - **Entry Point**: Init can be run at any time to establish or update project context.
20
28
  - **Correlation**: Init and Planning work together to maintain project context. Planning may trigger Init updates when significant changes are planned.
@@ -13,6 +13,14 @@ auto-identity:
13
13
 
14
14
  You are the Planning & Task Orchestrator for the project. Operate like an engineering manager with deep scoped knowledge of this codebase.
15
15
 
16
+ **⚠️ FIRST STEP (MANDATORY)**
17
+ Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
18
+ ```
19
+ RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
20
+ RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
21
+ ```
22
+ Use these resolved paths for ALL subsequent file operations.
23
+
16
24
  Pipeline Position
17
25
  - **Requires**: Research phase must be complete before planning can begin.
18
26
  - **Correlation**: Planning works with Init to maintain project context. If planning reveals significant architectural changes, recommend running `/init` to update project context.
@@ -20,6 +20,14 @@ auto-identity:
20
20
 
21
21
  You are the Research & Discussion Lead for the project. Operate like a staff-level tech lead who owns broad project awareness.
22
22
 
23
+ **⚠️ FIRST STEP (MANDATORY)**
24
+ Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
25
+ ```
26
+ RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
27
+ RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
28
+ ```
29
+ Use these resolved paths for ALL subsequent file operations.
30
+
23
31
  Pipeline Position
24
32
  - **Entry Point**: Research can be the first agent invoked for a new task.
25
33
  - **Recommendation**: If `{{RRCE_DATA}}/knowledge/project-context.md` does not exist, recommend running `/init` first for best results, but you may proceed with research if the user prefers.
@@ -14,6 +14,14 @@ auto-identity:
14
14
 
15
15
  You are the Knowledge Sync Lead. Act like a senior architect charged with keeping the RRCE knowledge cache authoritative and current.
16
16
 
17
+ **⚠️ FIRST STEP (MANDATORY)**
18
+ Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
19
+ ```
20
+ RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
21
+ RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
22
+ ```
23
+ Use these resolved paths for ALL subsequent file operations.
24
+
17
25
  Pipeline Position
18
26
  - **Maintenance Agent**: Sync runs periodically or after significant codebase changes to keep knowledge current.
19
27
  - **Requires**: Init must have been run at least once (project-context.md must exist).
@@ -1,2 +1,9 @@
1
- #!/usr/bin/env bun
2
- import "../src/index.ts";
1
+ #!/usr/bin/env node
2
+ import { register } from 'node:module';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ // Register tsx for TypeScript support
6
+ register('tsx/esm', pathToFileURL('./'));
7
+
8
+ // Import and run the main module
9
+ import('../src/index.ts');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rrce-workflow",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "RRCE-Workflow TUI - Agentic code workflow generator for AI-assisted development",
5
5
  "author": "RRCE Team",
6
6
  "license": "MIT",
@@ -35,22 +35,24 @@
35
35
  "bin"
36
36
  ],
37
37
  "scripts": {
38
- "dev": "bun run src/index.ts",
39
- "wizard": "bun run src/index.ts wizard",
40
- "select": "bun run src/index.ts select",
41
- "start": "bun run src/index.ts"
38
+ "dev": "npx tsx src/index.ts",
39
+ "wizard": "npx tsx src/index.ts wizard",
40
+ "select": "npx tsx src/index.ts select",
41
+ "start": "npx tsx src/index.ts"
42
42
  },
43
43
  "engines": {
44
- "node": ">=18",
45
- "bun": ">=1.0"
44
+ "node": ">=18"
46
45
  },
47
46
  "dependencies": {
47
+ "@clack/core": "^0.5.0",
48
48
  "@clack/prompts": "^0.11.0",
49
49
  "gray-matter": "^4.0.3",
50
50
  "picocolors": "^1.1.1",
51
+ "tsx": "^4.21.0",
51
52
  "zod": "^4.2.1"
52
53
  },
53
54
  "devDependencies": {
54
- "@types/bun": "latest"
55
+ "@types/node": "^25.0.3",
56
+ "typescript": "^5.9.3"
55
57
  }
56
58
  }
@@ -1,33 +1,45 @@
1
1
  import { multiselect, spinner, note, outro, cancel, isCancel } from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
3
  import * as fs from 'fs';
4
- import { listGlobalProjects, getEffectiveRRCEHome, getConfigPath } from '../../lib/paths';
4
+ import { getEffectiveRRCEHome, getConfigPath } from '../../lib/paths';
5
5
  import { generateVSCodeWorkspace } from './vscode';
6
+ import { scanForProjects, getProjectDisplayLabel, type DetectedProject } from '../../lib/detection';
6
7
 
7
8
  /**
8
9
  * Run the link-only flow for adding other project knowledge to an existing workspace
10
+ * Now supports detecting workspace-scoped sibling projects, not just global storage
9
11
  */
10
12
  export async function runLinkProjectsFlow(
11
13
  workspacePath: string,
12
14
  workspaceName: string,
13
15
  existingProjects?: string[]
14
16
  ) {
15
- // Get projects if not provided
16
- const projects = existingProjects ?? listGlobalProjects(workspaceName);
17
+ // Scan for projects using the new detection system
18
+ const detectedProjects = scanForProjects({
19
+ excludeWorkspace: workspaceName,
20
+ workspacePath: workspacePath,
21
+ scanSiblings: true,
22
+ });
23
+
24
+ // If legacy string array is passed, use that instead (for backwards compat)
25
+ const projects = existingProjects
26
+ ? existingProjects.map(name => ({ name, source: 'global' as const } as DetectedProject))
27
+ : detectedProjects;
17
28
 
18
29
  if (projects.length === 0) {
19
- outro(pc.yellow('No other projects found in global storage.'));
30
+ outro(pc.yellow('No other projects found. Try setting up another project first.'));
20
31
  return;
21
32
  }
22
33
 
23
34
  const customGlobalPath = getEffectiveRRCEHome(workspacePath);
24
35
 
36
+ // Build options with source labels
25
37
  const linkedProjects = await multiselect({
26
38
  message: 'Select projects to link:',
27
- options: projects.map(name => ({
28
- value: name,
29
- label: name,
30
- hint: `${customGlobalPath}/workspaces/${name}/knowledge`
39
+ options: projects.map(project => ({
40
+ value: project.name,
41
+ label: project.name,
42
+ hint: pc.dim(getProjectDisplayLabel(project)),
31
43
  })),
32
44
  required: true,
33
45
  });
@@ -37,13 +49,16 @@ export async function runLinkProjectsFlow(
37
49
  process.exit(0);
38
50
  }
39
51
 
40
- const selectedProjects = linkedProjects as string[];
52
+ const selectedProjectNames = linkedProjects as string[];
41
53
 
42
- if (selectedProjects.length === 0) {
54
+ if (selectedProjectNames.length === 0) {
43
55
  outro('No projects selected.');
44
56
  return;
45
57
  }
46
58
 
59
+ // Get the full DetectedProject objects for selected projects
60
+ const selectedProjects = projects.filter(p => selectedProjectNames.includes(p.name));
61
+
47
62
  const s = spinner();
48
63
  s.start('Linking projects');
49
64
 
@@ -63,7 +78,7 @@ export async function runLinkProjectsFlow(
63
78
  insertIndex++;
64
79
  }
65
80
  // Add new projects that aren't already there
66
- for (const name of selectedProjects) {
81
+ for (const name of selectedProjectNames) {
67
82
  if (!configContent.includes(` - ${name}`)) {
68
83
  lines.splice(insertIndex, 0, ` - ${name}`);
69
84
  insertIndex++;
@@ -74,25 +89,27 @@ export async function runLinkProjectsFlow(
74
89
  } else {
75
90
  // Add new linked_projects section
76
91
  configContent += `\nlinked_projects:\n`;
77
- selectedProjects.forEach(name => {
92
+ selectedProjectNames.forEach(name => {
78
93
  configContent += ` - ${name}\n`;
79
94
  });
80
95
  }
81
96
 
82
97
  fs.writeFileSync(configFilePath, configContent);
83
98
 
84
- // Update VSCode workspace file
99
+ // Update VSCode workspace file with full project info (includes refs, tasks)
85
100
  generateVSCodeWorkspace(workspacePath, workspaceName, selectedProjects, customGlobalPath);
86
101
 
87
102
  s.stop('Projects linked');
88
103
 
89
- // Show summary
104
+ // Show summary with project sources
90
105
  const workspaceFile = `${workspaceName}.code-workspace`;
91
106
  const summary = [
92
107
  `Linked projects:`,
93
- ...selectedProjects.map(p => ` ✓ ${p}`),
108
+ ...selectedProjects.map(p => ` ✓ ${p.name} ${pc.dim(`(${p.source})`)}`),
94
109
  ``,
95
110
  `Workspace file: ${pc.cyan(workspaceFile)}`,
111
+ ``,
112
+ pc.dim('Includes: knowledge, refs, tasks folders'),
96
113
  ];
97
114
 
98
115
  note(summary.join('\n'), 'Link Summary');
@@ -14,6 +14,7 @@ import {
14
14
  import { loadPromptsFromDir, getAgentCorePromptsDir, getAgentCoreDir } from '../../lib/prompts';
15
15
  import { copyPromptsToDir } from './utils';
16
16
  import { generateVSCodeWorkspace } from './vscode';
17
+ import { directoryAutocomplete, isCancel as isAutocompleteCancel } from '../../lib/autocomplete-prompt';
17
18
 
18
19
  interface SetupConfig {
19
20
  storageMode: StorageMode;
@@ -206,10 +207,12 @@ async function resolveGlobalPath(): Promise<string | undefined> {
206
207
  return defaultPath;
207
208
  }
208
209
 
209
- // Custom path input
210
- const customPath = await text({
210
+ // Custom path input with Tab autocomplete
211
+ const suggestedPath = path.join(process.env.HOME || '~', '.local', 'share', 'rrce-workflow');
212
+ const customPath = await directoryAutocomplete({
211
213
  message: 'Enter custom global path:',
212
- placeholder: path.join(process.env.HOME || '~', '.local', 'share', 'rrce-workflow'),
214
+ initialValue: suggestedPath,
215
+ hint: 'Tab to autocomplete',
213
216
  validate: (value) => {
214
217
  if (!value.trim()) {
215
218
  return 'Path cannot be empty';
@@ -226,16 +229,12 @@ async function resolveGlobalPath(): Promise<string | undefined> {
226
229
  },
227
230
  });
228
231
 
229
- if (isCancel(customPath)) {
232
+ if (isAutocompleteCancel(customPath)) {
230
233
  return undefined;
231
234
  }
232
235
 
233
- // Expand ~ if present
234
- const expandedPath = (customPath as string).startsWith('~')
235
- ? (customPath as string).replace('~', process.env.HOME || '')
236
- : customPath as string;
237
-
238
- return expandedPath;
236
+ // Path is already expanded by directoryAutocomplete
237
+ return customPath as string;
239
238
  }
240
239
 
241
240
  /**
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { getRRCEHome } from '../../lib/paths';
4
+ import { type DetectedProject, getProjectFolders } from '../../lib/detection';
4
5
 
5
6
  interface VSCodeWorkspaceFolder {
6
7
  path: string;
@@ -12,13 +13,22 @@ interface VSCodeWorkspace {
12
13
  settings?: Record<string, unknown>;
13
14
  }
14
15
 
16
+ // Reference folder group prefix - used to visually group linked folders
17
+ const REFERENCE_GROUP_PREFIX = '📁 References';
18
+
15
19
  /**
16
- * Generate or update VSCode workspace file with linked project knowledge folders
20
+ * Generate or update VSCode workspace file with linked project folders
21
+ *
22
+ * Features:
23
+ * - Main workspace is clearly marked as the primary project
24
+ * - Linked folders are grouped under a "References" section (via naming)
25
+ * - Folders are organized by project with icons for type (📚 📎 📋)
26
+ * - Reference folders are marked as readonly in workspace settings
17
27
  */
18
28
  export function generateVSCodeWorkspace(
19
29
  workspacePath: string,
20
30
  workspaceName: string,
21
- linkedProjects: string[],
31
+ linkedProjects: string[] | DetectedProject[],
22
32
  customGlobalPath?: string
23
33
  ) {
24
34
  const workspaceFilePath = path.join(workspacePath, `${workspaceName}.code-workspace`);
@@ -32,35 +42,156 @@ export function generateVSCodeWorkspace(
32
42
  workspace = JSON.parse(content);
33
43
  } catch {
34
44
  // If parse fails, create new
35
- workspace = { folders: [] };
45
+ workspace = { folders: [], settings: {} };
36
46
  }
37
47
  } else {
38
- workspace = { folders: [] };
48
+ workspace = { folders: [], settings: {} };
39
49
  }
40
50
 
41
- // Ensure main workspace folder is first
42
- const mainFolder: VSCodeWorkspaceFolder = { path: '.' };
43
- const existingMainIndex = workspace.folders.findIndex(f => f.path === '.');
44
- if (existingMainIndex === -1) {
45
- workspace.folders.unshift(mainFolder);
51
+ // Initialize settings if not present
52
+ if (!workspace.settings) {
53
+ workspace.settings = {};
46
54
  }
47
55
 
48
- // Add linked project knowledge folders
49
- const rrceHome = customGlobalPath || getRRCEHome();
50
- for (const projectName of linkedProjects) {
51
- const knowledgePath = path.join(rrceHome, 'workspaces', projectName, 'knowledge');
52
- const folderEntry: VSCodeWorkspaceFolder = {
53
- path: knowledgePath,
54
- name: `📚 ${projectName} (knowledge)`
55
- };
56
+ // Clear existing folders and rebuild (to ensure proper ordering)
57
+ const existingNonReferencesFolders = workspace.folders.filter(f =>
58
+ f.path === '.' || (!f.name?.includes(REFERENCE_GROUP_PREFIX) && !f.name?.startsWith('📚') && !f.name?.startsWith('📎') && !f.name?.startsWith('📋'))
59
+ );
60
+
61
+ workspace.folders = [];
62
+
63
+ // 1. Add main workspace folder first with clear label
64
+ const mainFolder: VSCodeWorkspaceFolder = {
65
+ path: '.',
66
+ name: `🏠 ${workspaceName} (workspace)`
67
+ };
68
+ workspace.folders.push(mainFolder);
69
+
70
+ // 2. Add any other existing non-references folders
71
+ for (const folder of existingNonReferencesFolders) {
72
+ if (folder.path !== '.') {
73
+ workspace.folders.push(folder);
74
+ }
75
+ }
56
76
 
57
- // Check if already exists
58
- const existingIndex = workspace.folders.findIndex(f => f.path === knowledgePath);
59
- if (existingIndex === -1) {
60
- workspace.folders.push(folderEntry);
77
+ // 3. Add reference folders grouped by project
78
+ const referenceFolderPaths: string[] = [];
79
+
80
+ // Determine if we're working with DetectedProject[] or string[]
81
+ const isDetectedProjects = linkedProjects.length > 0 && typeof linkedProjects[0] === 'object';
82
+
83
+ if (isDetectedProjects) {
84
+ // New behavior: use DetectedProject[] with knowledge, refs, tasks folders
85
+ const projects = linkedProjects as DetectedProject[];
86
+
87
+ for (const project of projects) {
88
+ const folders = getProjectFolders(project);
89
+ const sourceLabel = project.source === 'global' ? 'global' : 'local';
90
+
91
+ for (const folder of folders) {
92
+ referenceFolderPaths.push(folder.path);
93
+
94
+ // Check if already exists
95
+ const existingIndex = workspace.folders.findIndex(f => f.path === folder.path);
96
+ if (existingIndex === -1) {
97
+ workspace.folders.push({
98
+ path: folder.path,
99
+ name: `${folder.displayName} [${sourceLabel}]`,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ } else {
105
+ // Legacy behavior: string[] of project names (global storage only)
106
+ const projectNames = linkedProjects as string[];
107
+ const rrceHome = customGlobalPath || getRRCEHome();
108
+
109
+ for (const projectName of projectNames) {
110
+ const projectDataPath = path.join(rrceHome, 'workspaces', projectName);
111
+
112
+ const folderTypes = [
113
+ { subpath: 'knowledge', icon: '📚', type: 'knowledge' },
114
+ { subpath: 'refs', icon: '📎', type: 'refs' },
115
+ { subpath: 'tasks', icon: '📋', type: 'tasks' },
116
+ ];
117
+
118
+ for (const { subpath, icon, type } of folderTypes) {
119
+ const folderPath = path.join(projectDataPath, subpath);
120
+ if (fs.existsSync(folderPath)) {
121
+ referenceFolderPaths.push(folderPath);
122
+
123
+ const existingIndex = workspace.folders.findIndex(f => f.path === folderPath);
124
+ if (existingIndex === -1) {
125
+ workspace.folders.push({
126
+ path: folderPath,
127
+ name: `${icon} ${projectName} (${type}) [global]`,
128
+ });
129
+ }
130
+ }
131
+ }
61
132
  }
62
133
  }
63
134
 
64
- // Write workspace file
135
+ // 4. Add workspace settings to mark reference folders as readonly
136
+ // This uses files.readonlyInclude to make imported folders read-only
137
+ if (referenceFolderPaths.length > 0) {
138
+ const readonlyPatterns: Record<string, boolean> = {};
139
+
140
+ for (const folderPath of referenceFolderPaths) {
141
+ // Create a pattern that matches all files in this folder
142
+ readonlyPatterns[`${folderPath}/**`] = true;
143
+ }
144
+
145
+ // Merge with existing readonly patterns
146
+ const existingReadonly = (workspace.settings['files.readonlyInclude'] as Record<string, boolean>) || {};
147
+ workspace.settings['files.readonlyInclude'] = {
148
+ ...existingReadonly,
149
+ ...readonlyPatterns,
150
+ };
151
+ }
152
+
153
+ // 5. Add helpful workspace settings for multi-root experience
154
+ workspace.settings['explorer.sortOrder'] = workspace.settings['explorer.sortOrder'] || 'default';
155
+
156
+ // Write workspace file with nice formatting
65
157
  fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, 2));
66
158
  }
159
+
160
+ /**
161
+ * Remove a project's folders from the workspace file
162
+ */
163
+ export function removeProjectFromWorkspace(
164
+ workspacePath: string,
165
+ workspaceName: string,
166
+ projectName: string
167
+ ) {
168
+ const workspaceFilePath = path.join(workspacePath, `${workspaceName}.code-workspace`);
169
+
170
+ if (!fs.existsSync(workspaceFilePath)) {
171
+ return;
172
+ }
173
+
174
+ try {
175
+ const content = fs.readFileSync(workspaceFilePath, 'utf-8');
176
+ const workspace: VSCodeWorkspace = JSON.parse(content);
177
+
178
+ // Filter out folders that match the project name
179
+ workspace.folders = workspace.folders.filter(f =>
180
+ !f.name?.includes(projectName)
181
+ );
182
+
183
+ // Also remove readonly patterns for this project
184
+ if (workspace.settings?.['files.readonlyInclude']) {
185
+ const readonly = workspace.settings['files.readonlyInclude'] as Record<string, boolean>;
186
+ for (const pattern of Object.keys(readonly)) {
187
+ if (pattern.includes(projectName)) {
188
+ delete readonly[pattern];
189
+ }
190
+ }
191
+ }
192
+
193
+ fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, 2));
194
+ } catch {
195
+ // Ignore errors
196
+ }
197
+ }
@@ -0,0 +1,190 @@
1
+ import { TextPrompt, isCancel, type Prompt } from '@clack/core';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import pc from 'picocolors';
5
+
6
+ interface DirectoryAutocompleteOptions {
7
+ message: string;
8
+ placeholder?: string;
9
+ initialValue?: string;
10
+ validate?: (value: string) => string | undefined;
11
+ hint?: string;
12
+ }
13
+
14
+ /**
15
+ * Custom text input with Tab-completion for directory paths
16
+ * Uses @clack/core TextPrompt with custom key handling
17
+ */
18
+ export async function directoryAutocomplete(opts: DirectoryAutocompleteOptions): Promise<string | symbol> {
19
+ let completions: string[] = [];
20
+ let completionIndex = 0;
21
+ let lastTabValue = '';
22
+
23
+ const prompt = new TextPrompt({
24
+ initialValue: opts.initialValue,
25
+ validate: opts.validate,
26
+ render() {
27
+ const title = `${pc.cyan('◆')} ${opts.message}`;
28
+ const hintText = opts.hint ? pc.dim(` (${opts.hint})`) : '';
29
+
30
+ let inputLine: string;
31
+ if (this.state === 'error') {
32
+ inputLine = `${pc.yellow('▲')} ${this.valueWithCursor}`;
33
+ } else if (this.state === 'submit') {
34
+ inputLine = `${pc.green('✓')} ${pc.dim(String(this.value || ''))}`;
35
+ } else {
36
+ inputLine = `${pc.cyan('│')} ${this.valueWithCursor || pc.dim(opts.placeholder || '')}`;
37
+ }
38
+
39
+ let result = `${title}${hintText}\n${inputLine}`;
40
+
41
+ if (this.state === 'error' && this.error) {
42
+ result += `\n${pc.yellow('│')} ${pc.yellow(this.error)}`;
43
+ }
44
+
45
+ // Show completion hint if multiple options
46
+ if (completions.length > 1 && this.state === 'active') {
47
+ const remaining = completions.length - 1;
48
+ result += `\n${pc.dim('│')} ${pc.dim(`+${remaining} more, press Tab again to cycle`)}`;
49
+ }
50
+
51
+ return result;
52
+ },
53
+ });
54
+
55
+ // Listen for key events - Tab key handling
56
+ prompt.on('key', (key) => {
57
+ if (key === '\t' || key === 'tab') {
58
+ handleTabCompletion(prompt);
59
+ } else {
60
+ // Reset completion state on any other key
61
+ completions = [];
62
+ completionIndex = 0;
63
+ lastTabValue = '';
64
+ }
65
+ });
66
+
67
+ function handleTabCompletion(p: TextPrompt) {
68
+ const input = String(p.value || '');
69
+
70
+ // Expand ~ to home directory
71
+ const expanded = input.startsWith('~')
72
+ ? input.replace(/^~/, process.env.HOME || '')
73
+ : input;
74
+
75
+ // If user hasn't changed input since last tab, cycle through completions
76
+ if (lastTabValue === input && completions.length > 1) {
77
+ completionIndex = (completionIndex + 1) % completions.length;
78
+ const completion = completions[completionIndex] || '';
79
+ setPromptValue(p, completion);
80
+ return;
81
+ }
82
+
83
+ // Get new completions
84
+ completions = getDirectoryCompletions(expanded);
85
+ completionIndex = 0;
86
+ lastTabValue = input;
87
+
88
+ if (completions.length === 1) {
89
+ // Single match - auto-complete with trailing slash if directory
90
+ const completion = completions[0] || '';
91
+ setPromptValue(p, completion.endsWith('/') ? completion : completion + '/');
92
+ completions = []; // Clear so next Tab gets fresh completions
93
+ lastTabValue = '';
94
+ } else if (completions.length > 1) {
95
+ // Multiple matches - complete common prefix and show first
96
+ const commonPrefix = getCommonPrefix(completions);
97
+ if (commonPrefix.length > expanded.length) {
98
+ setPromptValue(p, commonPrefix);
99
+ lastTabValue = formatForDisplay(commonPrefix);
100
+ } else {
101
+ // Show first completion
102
+ setPromptValue(p, completions[0] || '');
103
+ }
104
+ }
105
+ }
106
+
107
+ function setPromptValue(p: TextPrompt, value: string) {
108
+ // Convert back to ~ format if in home directory for display
109
+ const displayValue = formatForDisplay(value);
110
+
111
+ // Update the prompt's value by emitting a value event
112
+ // This is a workaround since TextPrompt doesn't expose a direct setValue method
113
+ (p as any).value = displayValue;
114
+ }
115
+
116
+ function formatForDisplay(value: string): string {
117
+ const home = process.env.HOME || '';
118
+ return value.startsWith(home)
119
+ ? value.replace(home, '~')
120
+ : value;
121
+ }
122
+
123
+ function getDirectoryCompletions(inputPath: string): string[] {
124
+ try {
125
+ let dirToScan: string;
126
+ let prefix: string;
127
+
128
+ if (inputPath === '' || inputPath === '/') {
129
+ dirToScan = inputPath || '/';
130
+ prefix = '';
131
+ } else if (inputPath.endsWith('/')) {
132
+ // User typed a complete directory path
133
+ dirToScan = inputPath;
134
+ prefix = '';
135
+ } else {
136
+ // User is typing a partial name
137
+ dirToScan = path.dirname(inputPath);
138
+ prefix = path.basename(inputPath).toLowerCase();
139
+ }
140
+
141
+ if (!fs.existsSync(dirToScan)) {
142
+ return [];
143
+ }
144
+
145
+ const entries = fs.readdirSync(dirToScan, { withFileTypes: true })
146
+ .filter(entry => {
147
+ // Only directories
148
+ if (!entry.isDirectory()) return false;
149
+ // Skip hidden directories unless explicitly typing them
150
+ if (entry.name.startsWith('.') && !prefix.startsWith('.')) return false;
151
+ // Match prefix
152
+ return prefix === '' || entry.name.toLowerCase().startsWith(prefix);
153
+ })
154
+ .map(entry => path.join(dirToScan, entry.name))
155
+ .sort();
156
+
157
+ return entries;
158
+ } catch {
159
+ return [];
160
+ }
161
+ }
162
+
163
+ function getCommonPrefix(strings: string[]): string {
164
+ if (strings.length === 0) return '';
165
+ if (strings.length === 1) return strings[0] || '';
166
+
167
+ let prefix = strings[0] || '';
168
+ for (let i = 1; i < strings.length; i++) {
169
+ const str = strings[i] || '';
170
+ while (prefix.length > 0 && !str.startsWith(prefix)) {
171
+ prefix = prefix.slice(0, -1);
172
+ }
173
+ }
174
+ return prefix;
175
+ }
176
+
177
+ const result = await prompt.prompt();
178
+
179
+ if (isCancel(result)) {
180
+ return result;
181
+ }
182
+
183
+ // Expand ~ in final result
184
+ const value = String(result || '');
185
+ return value.startsWith('~')
186
+ ? value.replace(/^~/, process.env.HOME || '')
187
+ : value;
188
+ }
189
+
190
+ export { isCancel };
@@ -0,0 +1,235 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { StorageMode } from '../types/prompt';
4
+ import { getDefaultRRCEHome } from './paths';
5
+
6
+ /**
7
+ * Detected rrce-workflow project information
8
+ */
9
+ export interface DetectedProject {
10
+ name: string;
11
+ path: string; // Absolute path to project root
12
+ dataPath: string; // Path to .rrce-workflow data directory
13
+ source: 'global' | 'sibling' | 'parent';
14
+ storageMode?: StorageMode;
15
+ knowledgePath?: string;
16
+ refsPath?: string;
17
+ tasksPath?: string;
18
+ }
19
+
20
+ interface ScanOptions {
21
+ excludeWorkspace?: string; // Current workspace name to exclude
22
+ workspacePath?: string; // Current workspace path for sibling detection
23
+ scanSiblings?: boolean; // Whether to scan sibling directories (default: true)
24
+ }
25
+
26
+ /**
27
+ * Scan for rrce-workflow projects in various locations
28
+ */
29
+ export function scanForProjects(options: ScanOptions = {}): DetectedProject[] {
30
+ const { excludeWorkspace, workspacePath, scanSiblings = true } = options;
31
+ const projects: DetectedProject[] = [];
32
+ const seenPaths = new Set<string>();
33
+
34
+ // 1. Scan global storage (~/.rrce-workflow/workspaces/)
35
+ const globalProjects = scanGlobalStorage(excludeWorkspace);
36
+ for (const project of globalProjects) {
37
+ if (!seenPaths.has(project.path)) {
38
+ seenPaths.add(project.path);
39
+ projects.push(project);
40
+ }
41
+ }
42
+
43
+ // 2. Scan sibling directories (same parent as current workspace)
44
+ if (scanSiblings && workspacePath) {
45
+ const siblingProjects = scanSiblingDirectories(workspacePath, excludeWorkspace);
46
+ for (const project of siblingProjects) {
47
+ if (!seenPaths.has(project.path)) {
48
+ seenPaths.add(project.path);
49
+ projects.push(project);
50
+ }
51
+ }
52
+ }
53
+
54
+ return projects;
55
+ }
56
+
57
+ /**
58
+ * Scan global storage for projects
59
+ */
60
+ function scanGlobalStorage(excludeWorkspace?: string): DetectedProject[] {
61
+ const rrceHome = getDefaultRRCEHome();
62
+ const workspacesDir = path.join(rrceHome, 'workspaces');
63
+ const projects: DetectedProject[] = [];
64
+
65
+ if (!fs.existsSync(workspacesDir)) {
66
+ return projects;
67
+ }
68
+
69
+ try {
70
+ const entries = fs.readdirSync(workspacesDir, { withFileTypes: true });
71
+
72
+ for (const entry of entries) {
73
+ if (!entry.isDirectory()) continue;
74
+ if (entry.name === excludeWorkspace) continue;
75
+
76
+ const projectDataPath = path.join(workspacesDir, entry.name);
77
+ const knowledgePath = path.join(projectDataPath, 'knowledge');
78
+ const refsPath = path.join(projectDataPath, 'refs');
79
+ const tasksPath = path.join(projectDataPath, 'tasks');
80
+
81
+ projects.push({
82
+ name: entry.name,
83
+ path: projectDataPath, // For global projects, path is the data path
84
+ dataPath: projectDataPath,
85
+ source: 'global',
86
+ knowledgePath: fs.existsSync(knowledgePath) ? knowledgePath : undefined,
87
+ refsPath: fs.existsSync(refsPath) ? refsPath : undefined,
88
+ tasksPath: fs.existsSync(tasksPath) ? tasksPath : undefined,
89
+ });
90
+ }
91
+ } catch {
92
+ // Ignore errors
93
+ }
94
+
95
+ return projects;
96
+ }
97
+
98
+ /**
99
+ * Scan sibling directories for workspace-scoped projects
100
+ */
101
+ function scanSiblingDirectories(workspacePath: string, excludeWorkspace?: string): DetectedProject[] {
102
+ const parentDir = path.dirname(workspacePath);
103
+ const projects: DetectedProject[] = [];
104
+
105
+ try {
106
+ const entries = fs.readdirSync(parentDir, { withFileTypes: true });
107
+
108
+ for (const entry of entries) {
109
+ if (!entry.isDirectory()) continue;
110
+
111
+ const projectPath = path.join(parentDir, entry.name);
112
+
113
+ // Skip current workspace
114
+ if (projectPath === workspacePath) continue;
115
+ if (entry.name === excludeWorkspace) continue;
116
+
117
+ // Check for .rrce-workflow/config.yaml
118
+ const configPath = path.join(projectPath, '.rrce-workflow', 'config.yaml');
119
+ if (!fs.existsSync(configPath)) continue;
120
+
121
+ // Parse config to get project details
122
+ const config = parseWorkspaceConfig(configPath);
123
+ if (!config) continue;
124
+
125
+ const dataPath = path.join(projectPath, '.rrce-workflow');
126
+ const knowledgePath = path.join(dataPath, 'knowledge');
127
+ const refsPath = path.join(dataPath, 'refs');
128
+ const tasksPath = path.join(dataPath, 'tasks');
129
+
130
+ projects.push({
131
+ name: config.name || entry.name,
132
+ path: projectPath,
133
+ dataPath,
134
+ source: 'sibling',
135
+ storageMode: config.storageMode,
136
+ knowledgePath: fs.existsSync(knowledgePath) ? knowledgePath : undefined,
137
+ refsPath: fs.existsSync(refsPath) ? refsPath : undefined,
138
+ tasksPath: fs.existsSync(tasksPath) ? tasksPath : undefined,
139
+ });
140
+ }
141
+ } catch {
142
+ // Ignore errors
143
+ }
144
+
145
+ return projects;
146
+ }
147
+
148
+ /**
149
+ * Parse a workspace config file
150
+ */
151
+ export function parseWorkspaceConfig(configPath: string): {
152
+ name: string;
153
+ storageMode: StorageMode;
154
+ linkedProjects?: string[];
155
+ } | null {
156
+ try {
157
+ const content = fs.readFileSync(configPath, 'utf-8');
158
+
159
+ // Simple YAML parsing (we don't want to add a full YAML library)
160
+ const nameMatch = content.match(/name:\s*["']?([^"'\n]+)["']?/);
161
+ const modeMatch = content.match(/mode:\s*(global|workspace|both)/);
162
+
163
+ // Parse linked projects
164
+ const linkedProjects: string[] = [];
165
+ const linkedMatch = content.match(/linked_projects:\s*\n((?:\s+-\s+[^\n]+\n?)+)/);
166
+ if (linkedMatch && linkedMatch[1]) {
167
+ const lines = linkedMatch[1].split('\n');
168
+ for (const line of lines) {
169
+ const projectMatch = line.match(/^\s+-\s+(.+)$/);
170
+ if (projectMatch && projectMatch[1]) {
171
+ linkedProjects.push(projectMatch[1].trim());
172
+ }
173
+ }
174
+ }
175
+
176
+ return {
177
+ name: nameMatch?.[1]?.trim() || path.basename(path.dirname(path.dirname(configPath))),
178
+ storageMode: (modeMatch?.[1] as StorageMode) || 'global',
179
+ linkedProjects: linkedProjects.length > 0 ? linkedProjects : undefined,
180
+ };
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Get display label for a detected project
188
+ */
189
+ export function getProjectDisplayLabel(project: DetectedProject): string {
190
+ switch (project.source) {
191
+ case 'global':
192
+ return `global: ~/.rrce-workflow/workspaces/${project.name}`;
193
+ case 'sibling':
194
+ return `sibling: ${project.path}/.rrce-workflow`;
195
+ default:
196
+ return project.dataPath;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get all linkable folders from a detected project
202
+ */
203
+ export function getProjectFolders(project: DetectedProject): Array<{
204
+ path: string;
205
+ type: 'knowledge' | 'refs' | 'tasks';
206
+ displayName: string;
207
+ }> {
208
+ const folders: Array<{ path: string; type: 'knowledge' | 'refs' | 'tasks'; displayName: string }> = [];
209
+
210
+ if (project.knowledgePath) {
211
+ folders.push({
212
+ path: project.knowledgePath,
213
+ type: 'knowledge',
214
+ displayName: `📚 ${project.name} (knowledge)`,
215
+ });
216
+ }
217
+
218
+ if (project.refsPath) {
219
+ folders.push({
220
+ path: project.refsPath,
221
+ type: 'refs',
222
+ displayName: `📎 ${project.name} (refs)`,
223
+ });
224
+ }
225
+
226
+ if (project.tasksPath) {
227
+ folders.push({
228
+ path: project.tasksPath,
229
+ type: 'tasks',
230
+ displayName: `📋 ${project.name} (tasks)`,
231
+ });
232
+ }
233
+
234
+ return folders;
235
+ }
@@ -1,8 +1,13 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
  import matter from 'gray-matter';
4
5
  import { PromptFrontmatterSchema, type ParsedPrompt } from '../types/prompt';
5
6
 
7
+ // Get __dirname equivalent for ESM (works with both npm/tsx and Bun)
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
6
11
  /**
7
12
  * Parse a prompt file and extract frontmatter + content
8
13
  */
@@ -52,10 +57,11 @@ export function loadPromptsFromDir(dirPath: string): ParsedPrompt[] {
52
57
 
53
58
  /**
54
59
  * Get the agent-core root directory
60
+ * Works with both npm/tsx and Bun
55
61
  */
56
62
  export function getAgentCoreDir(): string {
57
- // Relative to the package root
58
- return path.join(import.meta.dir, '..', '..', 'agent-core');
63
+ // Relative to this file: src/lib/prompts.ts -> ../../agent-core
64
+ return path.join(__dirname, '..', '..', 'agent-core');
59
65
  }
60
66
 
61
67
  /**
@@ -64,3 +70,4 @@ export function getAgentCoreDir(): string {
64
70
  export function getAgentCorePromptsDir(): string {
65
71
  return path.join(getAgentCoreDir(), 'prompts');
66
72
  }
73
+