poly-agent 1.0.3 → 1.0.5
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/dist/cli.js +206 -0
- package/dist/lib/prompt-loader.js +127 -0
- package/package.json +1 -1
- package/prompts/_shared/context-hook.md +84 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { intro, select, multiselect, spinner, log, outro, cancel, isCancel } from '@clack/prompts';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { PromptLoader } from './lib/prompt-loader.js';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
// Get the prompts directory
|
|
9
|
+
async function getPromptsDir() {
|
|
10
|
+
// Try multiple possible locations
|
|
11
|
+
const possiblePaths = [
|
|
12
|
+
path.join(process.cwd(), 'prompts'), // Local development
|
|
13
|
+
path.join(process.cwd(), 'node_modules', 'poly-agent', 'prompts'), // Installed package
|
|
14
|
+
];
|
|
15
|
+
// Try to resolve via require if available
|
|
16
|
+
try {
|
|
17
|
+
const packagePath = require.resolve('poly-agent/package.json');
|
|
18
|
+
const packageDir = path.dirname(packagePath);
|
|
19
|
+
possiblePaths.unshift(path.join(packageDir, 'prompts'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Not installed as package, continue
|
|
23
|
+
}
|
|
24
|
+
// Check which path actually exists
|
|
25
|
+
for (const promptsPath of possiblePaths) {
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(promptsPath);
|
|
28
|
+
return promptsPath;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Fallback to first option
|
|
35
|
+
return possiblePaths[0];
|
|
36
|
+
}
|
|
37
|
+
// Map IDE to target directory
|
|
38
|
+
const IDE_DIRECTORIES = {
|
|
39
|
+
'Cursor': '.cursor/commands',
|
|
40
|
+
'Antigravity': '.agent/workflows',
|
|
41
|
+
'Claude': '.claude/skills',
|
|
42
|
+
};
|
|
43
|
+
async function runInit() {
|
|
44
|
+
// Welcome message
|
|
45
|
+
intro('Welcome to PolyAgent CLI');
|
|
46
|
+
// Step 1: Select AI IDE
|
|
47
|
+
const selectedIDE = await select({
|
|
48
|
+
message: 'Choose the AI IDE you want to use:',
|
|
49
|
+
options: [
|
|
50
|
+
{ value: 'Cursor', label: 'Cursor' },
|
|
51
|
+
{ value: 'Claude', label: 'Claude' },
|
|
52
|
+
{ value: 'Antigravity', label: 'Antigravity' }
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
if (isCancel(selectedIDE)) {
|
|
56
|
+
cancel('Operation cancelled.');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
// Step 2: Load available prompts
|
|
60
|
+
const promptsDir = await getPromptsDir();
|
|
61
|
+
const loader = new PromptLoader(promptsDir);
|
|
62
|
+
const promptFiles = await loader.listPrompts();
|
|
63
|
+
if (promptFiles.length === 0) {
|
|
64
|
+
log.error('No prompts found in the prompts directory.');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
// Load prompt metadata for display
|
|
68
|
+
const promptOptions = await Promise.all(promptFiles.map(async (filename) => {
|
|
69
|
+
try {
|
|
70
|
+
const prompt = await loader.loadPrompt(filename);
|
|
71
|
+
return {
|
|
72
|
+
value: filename,
|
|
73
|
+
label: `${prompt.name} - ${prompt.description || 'No description'}`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
value: filename,
|
|
79
|
+
label: filename.replace('.md', '')
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}));
|
|
83
|
+
// Step 3: Multi-select prompts
|
|
84
|
+
const selectedPrompts = await multiselect({
|
|
85
|
+
message: 'Select prompts to install (use Space to select, Enter to continue):',
|
|
86
|
+
options: promptOptions,
|
|
87
|
+
required: true
|
|
88
|
+
});
|
|
89
|
+
if (isCancel(selectedPrompts)) {
|
|
90
|
+
cancel('Operation cancelled.');
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
// Step 4: Install prompts with shared instructions
|
|
94
|
+
const targetDir = IDE_DIRECTORIES[selectedIDE];
|
|
95
|
+
const fullTargetPath = path.join(process.cwd(), targetDir);
|
|
96
|
+
const s = spinner();
|
|
97
|
+
s.start(`Installing ${selectedPrompts.length} prompt(s) to ${targetDir}...`);
|
|
98
|
+
try {
|
|
99
|
+
// Create target directory if it doesn't exist
|
|
100
|
+
await fs.mkdir(fullTargetPath, { recursive: true });
|
|
101
|
+
// Install each selected prompt with shared instructions injected
|
|
102
|
+
const installPromises = selectedPrompts.map(async (filename) => {
|
|
103
|
+
// Load prompt with shared instructions injected
|
|
104
|
+
const prompt = await loader.loadPrompt(filename);
|
|
105
|
+
// Reconstruct the full file content with frontmatter
|
|
106
|
+
const fullContent = `---
|
|
107
|
+
name: ${prompt.name}
|
|
108
|
+
description: ${prompt.description}
|
|
109
|
+
---
|
|
110
|
+
${prompt.content}`;
|
|
111
|
+
if (selectedIDE === 'Claude') {
|
|
112
|
+
// For Claude, create a folder with the prompt name and SKILL.md inside
|
|
113
|
+
const promptName = path.basename(filename, '.md');
|
|
114
|
+
const skillFolderPath = path.join(fullTargetPath, promptName);
|
|
115
|
+
const skillFilePath = path.join(skillFolderPath, 'SKILL.md');
|
|
116
|
+
// Create the skill folder
|
|
117
|
+
await fs.mkdir(skillFolderPath, { recursive: true });
|
|
118
|
+
// Write the processed content to SKILL.md
|
|
119
|
+
await fs.writeFile(skillFilePath, fullContent, 'utf-8');
|
|
120
|
+
return `${promptName}/SKILL.md`;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// For other IDEs, write directly
|
|
124
|
+
const targetPath = path.join(fullTargetPath, filename);
|
|
125
|
+
await fs.writeFile(targetPath, fullContent, 'utf-8');
|
|
126
|
+
return filename;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const installedFiles = await Promise.all(installPromises);
|
|
130
|
+
s.stop(`✓ Installed ${installedFiles.length} prompt(s) successfully`);
|
|
131
|
+
// Success message
|
|
132
|
+
log.success(`Prompts installed to: ${targetDir}`);
|
|
133
|
+
log.info(`Installed files:`);
|
|
134
|
+
installedFiles.forEach(file => {
|
|
135
|
+
log.info(` - ${file}`);
|
|
136
|
+
});
|
|
137
|
+
outro('Installation complete! 🎉');
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
s.stop('✗ Installation failed');
|
|
141
|
+
log.error(`Failed to install prompts: ${error instanceof Error ? error.message : String(error)}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function showHelp() {
|
|
146
|
+
console.log(`
|
|
147
|
+
PolyAgent CLI - Install AI IDE prompts
|
|
148
|
+
|
|
149
|
+
Usage:
|
|
150
|
+
poly-agent init Initialize and install prompts
|
|
151
|
+
poly-agent --help Show this help message
|
|
152
|
+
poly-agent --version Show version
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
npx poly-agent init Install prompts for your AI IDE
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
function showVersion() {
|
|
159
|
+
try {
|
|
160
|
+
// Try to get version from installed package first
|
|
161
|
+
const packagePath = require.resolve('poly-agent/package.json');
|
|
162
|
+
const packageJson = require(packagePath);
|
|
163
|
+
console.log(`poly-agent v${packageJson.version}`);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
try {
|
|
167
|
+
// Fallback to local package.json for development
|
|
168
|
+
const packageJson = require('../package.json');
|
|
169
|
+
console.log(`poly-agent v${packageJson.version}`);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
console.log('poly-agent (version unknown)');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function main() {
|
|
177
|
+
const args = process.argv.slice(2);
|
|
178
|
+
const command = args[0];
|
|
179
|
+
switch (command) {
|
|
180
|
+
case 'init':
|
|
181
|
+
await runInit();
|
|
182
|
+
break;
|
|
183
|
+
case '--help':
|
|
184
|
+
case '-h':
|
|
185
|
+
case 'help':
|
|
186
|
+
showHelp();
|
|
187
|
+
break;
|
|
188
|
+
case '--version':
|
|
189
|
+
case '-v':
|
|
190
|
+
case 'version':
|
|
191
|
+
showVersion();
|
|
192
|
+
break;
|
|
193
|
+
case undefined:
|
|
194
|
+
// No command provided, default to init for backward compatibility
|
|
195
|
+
await runInit();
|
|
196
|
+
break;
|
|
197
|
+
default:
|
|
198
|
+
log.error(`Unknown command: ${command}`);
|
|
199
|
+
showHelp();
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
main().catch((error) => {
|
|
204
|
+
console.error(`Fatal error: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export class PromptLoader {
|
|
4
|
+
promptsDir;
|
|
5
|
+
sharedInstructionsCache = null;
|
|
6
|
+
constructor(promptsDir) {
|
|
7
|
+
this.promptsDir = promptsDir;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Load all shared instructions from the _shared directory.
|
|
11
|
+
* These will be appended to every prompt.
|
|
12
|
+
*/
|
|
13
|
+
async loadSharedInstructions() {
|
|
14
|
+
// Return cached instructions if available
|
|
15
|
+
if (this.sharedInstructionsCache !== null) {
|
|
16
|
+
return this.sharedInstructionsCache;
|
|
17
|
+
}
|
|
18
|
+
const sharedDir = path.join(this.promptsDir, '_shared');
|
|
19
|
+
const instructions = [];
|
|
20
|
+
try {
|
|
21
|
+
const files = await fs.readdir(sharedDir);
|
|
22
|
+
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
23
|
+
for (const file of mdFiles) {
|
|
24
|
+
const filePath = path.join(sharedDir, file);
|
|
25
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
26
|
+
// Parse frontmatter to get name, extract content
|
|
27
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
28
|
+
const match = fileContent.match(frontmatterRegex);
|
|
29
|
+
if (match) {
|
|
30
|
+
const metadata = this.parseFrontmatter(match[1]);
|
|
31
|
+
instructions.push({
|
|
32
|
+
name: metadata.name || path.basename(file, '.md'),
|
|
33
|
+
content: match[2].trim()
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// No frontmatter, use entire content
|
|
38
|
+
instructions.push({
|
|
39
|
+
name: path.basename(file, '.md'),
|
|
40
|
+
content: fileContent.trim()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// _shared directory doesn't exist or is empty - that's okay
|
|
47
|
+
// Return empty array, shared instructions are optional
|
|
48
|
+
}
|
|
49
|
+
this.sharedInstructionsCache = instructions;
|
|
50
|
+
return instructions;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clear the shared instructions cache.
|
|
54
|
+
* Useful if shared instructions are modified at runtime.
|
|
55
|
+
*/
|
|
56
|
+
clearCache() {
|
|
57
|
+
this.sharedInstructionsCache = null;
|
|
58
|
+
}
|
|
59
|
+
async loadPrompt(filename, variables = {}) {
|
|
60
|
+
const filePath = path.join(this.promptsDir, filename);
|
|
61
|
+
try {
|
|
62
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
63
|
+
// Basic Frontmatter parsing
|
|
64
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
65
|
+
const match = fileContent.match(frontmatterRegex);
|
|
66
|
+
let name;
|
|
67
|
+
let description;
|
|
68
|
+
let content;
|
|
69
|
+
if (!match) {
|
|
70
|
+
// Fallback if no frontmatter
|
|
71
|
+
name = path.basename(filename, '.md');
|
|
72
|
+
description = 'No description provided.';
|
|
73
|
+
content = fileContent;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const rawFrontmatter = match[1];
|
|
77
|
+
const metadata = this.parseFrontmatter(rawFrontmatter);
|
|
78
|
+
name = metadata.name || path.basename(filename, '.md');
|
|
79
|
+
description = metadata.description || '';
|
|
80
|
+
content = match[2];
|
|
81
|
+
}
|
|
82
|
+
// Inject variables
|
|
83
|
+
content = this.injectVariables(content, variables);
|
|
84
|
+
// Append shared instructions
|
|
85
|
+
const sharedInstructions = await this.loadSharedInstructions();
|
|
86
|
+
if (sharedInstructions.length > 0) {
|
|
87
|
+
const sharedContent = sharedInstructions
|
|
88
|
+
.map(inst => `\n---\n\n${inst.content}`)
|
|
89
|
+
.join('\n');
|
|
90
|
+
content = content + sharedContent;
|
|
91
|
+
}
|
|
92
|
+
return { name, description, content };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new Error(`Failed to load prompt ${filename}: ${error}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
parseFrontmatter(frontmatter) {
|
|
99
|
+
const lines = frontmatter.split('\n');
|
|
100
|
+
const metadata = {};
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
const [key, ...valueParts] = line.split(':');
|
|
103
|
+
if (key && valueParts.length > 0) {
|
|
104
|
+
metadata[key.trim()] = valueParts.join(':').trim();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return metadata;
|
|
108
|
+
}
|
|
109
|
+
injectVariables(content, variables) {
|
|
110
|
+
let result = content;
|
|
111
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
112
|
+
// Replace {{KEY}}
|
|
113
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
async listPrompts() {
|
|
118
|
+
try {
|
|
119
|
+
const files = await fs.readdir(this.promptsDir);
|
|
120
|
+
// Filter out _shared directory and only return .md files
|
|
121
|
+
return files.filter(f => f.endsWith('.md') && !f.startsWith('_'));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Context Hook
|
|
3
|
+
description: Shared instructions for automatic context dumping after file edits.
|
|
4
|
+
type: shared
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Context Management Hook
|
|
8
|
+
|
|
9
|
+
## Behavior
|
|
10
|
+
|
|
11
|
+
After **every file edit** you make, you MUST update the project context file. This ensures continuity across conversations and helps maintain a clear record of progress.
|
|
12
|
+
|
|
13
|
+
## When to Update Context
|
|
14
|
+
|
|
15
|
+
1. **After any file edit** - Immediately after modifying, creating, or deleting a file
|
|
16
|
+
2. **When the task context changes** - New goals, pivots, or scope changes
|
|
17
|
+
3. **When explicitly mentioned** - If the user references `.context/` or asks about context
|
|
18
|
+
4. **At conversation milestones** - Completing a feature, fixing a bug, reaching a checkpoint
|
|
19
|
+
|
|
20
|
+
## Context File Location & Naming
|
|
21
|
+
|
|
22
|
+
- **Directory**: `.context/`
|
|
23
|
+
- **Filename**: Generate a short, descriptive kebab-case name based on the current task
|
|
24
|
+
- Examples: `feature-auth.md`, `bugfix-api-timeout.md`, `refactor-database-layer.md`, `setup-ci-pipeline.md`
|
|
25
|
+
- **Persistence**: Use the SAME context file throughout a conversation/task. Only create a new one if the task fundamentally changes.
|
|
26
|
+
|
|
27
|
+
## Context File Format
|
|
28
|
+
|
|
29
|
+
Use this exact structure:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
---
|
|
33
|
+
created: [ISO timestamp when first created]
|
|
34
|
+
updated: [ISO timestamp of last update]
|
|
35
|
+
task: [One-line task description]
|
|
36
|
+
status: [in-progress | completed | blocked | paused]
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# Context: [short-name]
|
|
40
|
+
|
|
41
|
+
## Summary
|
|
42
|
+
[2-3 sentence AI-generated summary of what this task is about and current state]
|
|
43
|
+
|
|
44
|
+
## Files Changed
|
|
45
|
+
- `path/to/file.ts` - [Brief description of change]
|
|
46
|
+
- `path/to/another.ts` - [Brief description of change]
|
|
47
|
+
|
|
48
|
+
## Current Goals
|
|
49
|
+
1. [Primary goal]
|
|
50
|
+
2. [Secondary goal]
|
|
51
|
+
3. [etc.]
|
|
52
|
+
|
|
53
|
+
## Progress
|
|
54
|
+
- [x] [Completed item]
|
|
55
|
+
- [x] [Completed item]
|
|
56
|
+
- [ ] [Pending item]
|
|
57
|
+
- [ ] [Pending item]
|
|
58
|
+
|
|
59
|
+
## Next Steps
|
|
60
|
+
- [Immediate next action]
|
|
61
|
+
- [Following action]
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
[Any important context, decisions made, blockers, or things to remember]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Rules
|
|
68
|
+
|
|
69
|
+
1. **Always update, never skip** - Even small edits warrant a context update
|
|
70
|
+
2. **Be concise but complete** - Capture essential information without verbosity
|
|
71
|
+
3. **Track ALL file changes** - Every file you touch should be listed
|
|
72
|
+
4. **Maintain history** - Don't remove completed items from Progress, mark them done
|
|
73
|
+
5. **Update timestamps** - Always update the `updated` field in frontmatter
|
|
74
|
+
6. **Create directory if needed** - If `.context/` doesn't exist, create it
|
|
75
|
+
|
|
76
|
+
## Example Workflow
|
|
77
|
+
|
|
78
|
+
1. User asks: "Add authentication to the API"
|
|
79
|
+
2. You create/update `.context/feature-api-auth.md` with initial goals
|
|
80
|
+
3. You edit `src/auth/middleware.ts`
|
|
81
|
+
4. You immediately update `.context/feature-api-auth.md` with the file change
|
|
82
|
+
5. You edit `src/routes/login.ts`
|
|
83
|
+
6. You update `.context/feature-api-auth.md` again
|
|
84
|
+
7. Continue until task complete, then set status to `completed`
|