Explorar o código

feat: use message insertion pattern for persistence and add project skills support

- Add project-local skills directory (.opencode/skills/) with highest priority
- Use client.session.prompt() with noReply for skill loading to persist across compaction
- Add session.compacted event handler to re-inject bootstrap after context compaction
- Update find_skills to list skills from project > personal > superpowers directories
- Support project: prefix for explicit project skill loading
- Keep chat.message hook as fallback for initial context injection
Jesse Vincent hai 9 meses
pai
achega
9cd6c52acc
Modificáronse 1 ficheiros con 106 adicións e 13 borrados
  1. 106 13
      .opencode/plugin/superpowers.js

+ 106 - 13
.opencode/plugin/superpowers.js

@@ -16,6 +16,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 export const SuperpowersPlugin = async ({ project, client, $, directory, worktree }) => {
   const homeDir = os.homedir();
+  const projectSkillsDir = path.join(directory, '.opencode/skills');
   const superpowersSkillsDir = path.join(homeDir, '.config/opencode/superpowers/skills');
   const personalSkillsDir = path.join(homeDir, '.config/opencode/skills');
   const promptsDir = path.join(homeDir, '.config/opencode/prompts');
@@ -26,11 +27,35 @@ export const SuperpowersPlugin = async ({ project, client, $, directory, worktre
       use_skill: tool({
         description: 'Load and read a specific skill to guide your work. Skills contain proven workflows, mandatory processes, and expert techniques.',
         args: {
-          skill_name: tool.schema.string().describe('Name of the skill to load (e.g., "superpowers:brainstorming" or "my-custom-skill")')
+          skill_name: tool.schema.string().describe('Name of the skill to load (e.g., "superpowers:brainstorming", "my-custom-skill", or "project:my-skill")')
         },
         execute: async (args, context) => {
           const { skill_name } = args;
-          const resolved = skillsCore.resolveSkillPath(skill_name, superpowersSkillsDir, personalSkillsDir);
+
+          // Resolve with priority: project > personal > superpowers
+          // Check for project: prefix first
+          const forceProject = skill_name.startsWith('project:');
+          const actualSkillName = forceProject ? skill_name.replace(/^project:/, '') : skill_name;
+
+          let resolved = null;
+
+          // Try project skills first (if project: prefix or no prefix)
+          if (forceProject || !skill_name.startsWith('superpowers:')) {
+            const projectPath = path.join(projectSkillsDir, actualSkillName);
+            const projectSkillFile = path.join(projectPath, 'SKILL.md');
+            if (fs.existsSync(projectSkillFile)) {
+              resolved = {
+                skillFile: projectSkillFile,
+                sourceType: 'project',
+                skillPath: actualSkillName
+              };
+            }
+          }
+
+          // Fall back to personal/superpowers resolution
+          if (!resolved && !forceProject) {
+            resolved = skillsCore.resolveSkillPath(skill_name, superpowersSkillsDir, personalSkillsDir);
+          }
 
           if (!resolved) {
             return `Error: Skill "${skill_name}" not found.\n\nRun find_skills to see available skills.`;
@@ -41,30 +66,60 @@ export const SuperpowersPlugin = async ({ project, client, $, directory, worktre
           const content = skillsCore.stripFrontmatter(fullContent);
           const skillDirectory = path.dirname(resolved.skillFile);
 
-          return `# ${name || skill_name}
+          const skillHeader = `# ${name || skill_name}
 # ${description || ''}
 # Supporting tools and docs are in ${skillDirectory}
-# ============================================
+# ============================================`;
+
+          // Insert as user message with noReply for persistence across compaction
+          try {
+            await client.session.prompt({
+              path: { id: context.sessionID },
+              body: {
+                noReply: true,
+                parts: [
+                  { type: "text", text: `Loading skill: ${name || skill_name}` },
+                  { type: "text", text: `${skillHeader}\n\n${content}` }
+                ]
+              }
+            });
+          } catch (err) {
+            // Fallback: return content directly if message insertion fails
+            return `${skillHeader}\n\n${content}`;
+          }
 
-${content}`;
+          return `Launching skill: ${name || skill_name}`;
         }
       }),
       find_skills: tool({
-        description: 'List all available skills in the superpowers and personal skill libraries.',
+        description: 'List all available skills in the project, personal, and superpowers skill libraries.',
         args: {},
         execute: async (args, context) => {
-          const superpowersSkills = skillsCore.findSkillsInDir(superpowersSkillsDir, 'superpowers', 3);
+          const projectSkills = skillsCore.findSkillsInDir(projectSkillsDir, 'project', 3);
           const personalSkills = skillsCore.findSkillsInDir(personalSkillsDir, 'personal', 3);
-          const allSkills = [...personalSkills, ...superpowersSkills];
+          const superpowersSkills = skillsCore.findSkillsInDir(superpowersSkillsDir, 'superpowers', 3);
+
+          // Priority: project > personal > superpowers
+          const allSkills = [...projectSkills, ...personalSkills, ...superpowersSkills];
 
           if (allSkills.length === 0) {
-            return 'No skills found. Install superpowers skills to ~/.config/opencode/superpowers/skills/';
+            return 'No skills found. Install superpowers skills to ~/.config/opencode/superpowers/skills/ or add project skills to .opencode/skills/';
           }
 
           let output = 'Available skills:\n\n';
 
           for (const skill of allSkills) {
-            const namespace = skill.sourceType === 'personal' ? '' : 'superpowers:';
+            let namespace;
+            switch (skill.sourceType) {
+              case 'project':
+                namespace = 'project:';
+                break;
+              case 'personal':
+                namespace = '';
+                break;
+              default:
+                namespace = 'superpowers:';
+            }
             const skillName = skill.name || path.basename(skill.path);
 
             output += `${namespace}${skillName}\n`;
@@ -94,10 +149,11 @@ When skills reference tools you don't have, substitute OpenCode equivalents:
 - \`Skill\` tool → \`use_skill\` custom tool
 - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
 
-**Skills naming:**
+**Skills naming (priority order):**
+- Project skills: \`project:skill-name\` (in .opencode/skills/)
+- Personal skills: \`skill-name\` (in ~/.config/opencode/skills/)
 - Superpowers skills: \`superpowers:skill-name\`
-- Personal skills: \`skill-name\`
-- Personal skills override superpowers when names match`;
+- Project skills override personal, which override superpowers when names match`;
 
           output.message.system = `<EXTREMELY_IMPORTANT>
 You have superpowers.
@@ -108,6 +164,43 @@ ${toolMapping}
 </EXTREMELY_IMPORTANT>`;
         }
       }
+    },
+    event: async ({ event }) => {
+      // Re-inject bootstrap after context compaction to maintain superpowers
+      if (event.type === 'session.compacted') {
+        const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
+
+        if (usingSuperpowersPath) {
+          const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
+          const content = skillsCore.stripFrontmatter(fullContent);
+
+          const toolMapping = `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill
+
+**Skills naming (priority order):** project: > personal > superpowers:`;
+
+          try {
+            await client.session.prompt({
+              path: { id: event.properties.sessionID },
+              body: {
+                noReply: true,
+                parts: [{
+                  type: "text",
+                  text: `<EXTREMELY_IMPORTANT>
+You have superpowers.
+
+${content}
+
+${toolMapping}
+</EXTREMELY_IMPORTANT>`
+                }]
+              }
+            });
+          } catch (err) {
+            // Silent failure - bootstrap will be missing but session continues
+            console.error('Failed to re-inject superpowers after compaction:', err.message);
+          }
+        }
+      }
     }
   };
 };