opencode-sa-assistant 0.2.2 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sa-assistant",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "description": "OpenCode plugin for AWS Solutions Architect assistant with multi-agent Guru system",
@@ -8,9 +8,10 @@
8
8
  import { SA_ORCHESTRATOR_PROMPT } from "../prompts/orchestrator";
9
9
  import { GURU_PROMPTS } from "../prompts/gurus";
10
10
  import { SPECIALIST_PROMPTS } from "../prompts/specialists";
11
- import { existsSync, mkdirSync, writeFileSync } from "fs";
12
- import { join } from "path";
11
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
12
+ import { join, dirname } from "path";
13
13
  import { homedir } from "os";
14
+ import { fileURLToPath } from "url";
14
15
 
15
16
  /**
16
17
  * Agent definition with frontmatter metadata
@@ -164,3 +165,110 @@ export function uninstallSAAgents(): string[] {
164
165
 
165
166
  return removed;
166
167
  }
168
+
169
+ /**
170
+ * SA Skill names that will be installed
171
+ */
172
+ export const SA_SKILL_NAMES = ["docx", "pptx", "mcp", "guru"] as const;
173
+
174
+ /**
175
+ * Get the skills directory path for project-level installation
176
+ * Uses process.cwd() to find the project root
177
+ */
178
+ function getSkillsDir(): string {
179
+ return join(process.cwd(), ".opencode", "skills");
180
+ }
181
+
182
+ /**
183
+ * Get the source skills directory (where SKILL.md files are bundled)
184
+ */
185
+ function getSourceSkillsDir(): string {
186
+ // In ESM, use import.meta.url to find the module location
187
+ // Fallback to __dirname for CommonJS compatibility
188
+ try {
189
+ const currentFile = fileURLToPath(import.meta.url);
190
+ return join(dirname(currentFile), "..", "skills");
191
+ } catch {
192
+ // Fallback for environments where import.meta.url is not available
193
+ return join(__dirname, "..", "skills");
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Install SA skills to .opencode/skills/
199
+ * Creates the directory structure if it doesn't exist.
200
+ * Only writes files if they don't exist (preserves user modifications).
201
+ */
202
+ export function installSASkills(): { installed: string[]; skipped: string[]; errors: string[] } {
203
+ const skillsDir = getSkillsDir();
204
+ const sourceDir = getSourceSkillsDir();
205
+ const installed: string[] = [];
206
+ const skipped: string[] = [];
207
+ const errors: string[] = [];
208
+
209
+ for (const skillName of SA_SKILL_NAMES) {
210
+ const targetDir = join(skillsDir, skillName);
211
+ const targetPath = join(targetDir, "SKILL.md");
212
+ const sourcePath = join(sourceDir, skillName, "SKILL.md");
213
+
214
+ try {
215
+ // Check if target already exists
216
+ if (existsSync(targetPath)) {
217
+ skipped.push(skillName);
218
+ continue;
219
+ }
220
+
221
+ // Read source file
222
+ if (!existsSync(sourcePath)) {
223
+ errors.push(`${skillName}: source file not found at ${sourcePath}`);
224
+ continue;
225
+ }
226
+
227
+ const content = readFileSync(sourcePath, "utf-8");
228
+
229
+ // Create target directory
230
+ if (!existsSync(targetDir)) {
231
+ mkdirSync(targetDir, { recursive: true });
232
+ }
233
+
234
+ // Write skill file
235
+ writeFileSync(targetPath, content, "utf-8");
236
+ installed.push(skillName);
237
+ } catch (err) {
238
+ errors.push(`${skillName}: ${err instanceof Error ? err.message : String(err)}`);
239
+ }
240
+ }
241
+
242
+ return { installed, skipped, errors };
243
+ }
244
+
245
+ /**
246
+ * Uninstall SA skills (remove SKILL.md files and directories)
247
+ */
248
+ export function uninstallSASkills(): string[] {
249
+ const skillsDir = getSkillsDir();
250
+ const removed: string[] = [];
251
+
252
+ for (const skillName of SA_SKILL_NAMES) {
253
+ const skillDir = join(skillsDir, skillName);
254
+ const skillPath = join(skillDir, "SKILL.md");
255
+
256
+ if (existsSync(skillPath)) {
257
+ try {
258
+ const { unlinkSync, rmdirSync } = require("fs");
259
+ unlinkSync(skillPath);
260
+ // Try to remove the directory if empty
261
+ try {
262
+ rmdirSync(skillDir);
263
+ } catch {
264
+ // Directory not empty, leave it
265
+ }
266
+ removed.push(skillName);
267
+ } catch {
268
+ // Ignore errors during uninstall
269
+ }
270
+ }
271
+ }
272
+
273
+ return removed;
274
+ }
@@ -2,59 +2,105 @@
2
2
  * WADD Mode Activation System
3
3
  *
4
4
  * Detects "wadd" keyword and activates AWS Solutions Architect mode.
5
- * Uses word boundary regex to prevent false positives.
5
+ * Follows oh-my-opencode keyword-detector pattern.
6
6
  */
7
7
 
8
8
  import { SA_ORCHESTRATOR_PROMPT } from "../prompts/orchestrator";
9
9
 
10
+ /**
11
+ * Patterns for code block removal (prevent false positives)
12
+ */
13
+ const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g;
14
+ const INLINE_CODE_PATTERN = /`[^`]+`/g;
15
+
10
16
  /**
11
17
  * Regex pattern for WADD keyword detection
12
- * - \b: Word boundary (prevents matching "wa", "wadding", "mywadd")
18
+ * - \b: Word boundary (prevents matching "wadding", "mywadd")
13
19
  * - i flag: Case-insensitive matching
14
20
  */
15
21
  const WADD_PATTERN = /\bwadd\b/i;
16
22
 
17
23
  /**
18
- * Detects if content contains the WADD keyword
24
+ * SA Mode activation message (injected before user message)
25
+ */
26
+ const SA_MODE_MESSAGE = `[sa-mode]
27
+ AWS Solutions Architect Mode activated. You have access to:
28
+ - 4 Guru agents (sa-bezos, sa-vogels, sa-naval, sa-feynman)
29
+ - 3 Specialist agents (sa-explorer, sa-researcher, sa-reviewer)
30
+ - AWS MCP tools (aws-docs_search_documentation, aws-docs_read_documentation)
31
+
32
+ Follow Guru_Mandate: Consult appropriate Gurus before architecture decisions.
33
+ Apply Well-Architected Framework 6 pillars for all reviews.`;
34
+
35
+ /**
36
+ * Removes code blocks from text to prevent false keyword detection
37
+ */
38
+ export function removeCodeBlocks(text: string): string {
39
+ return text.replace(CODE_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "");
40
+ }
41
+
42
+ /**
43
+ * Detects if content contains the WADD keyword (outside code blocks)
19
44
  * @param content - User message content to check
20
45
  * @returns true if WADD keyword found, false otherwise
21
46
  */
22
47
  export function detectWaddKeyword(content: string): boolean {
23
- return WADD_PATTERN.test(content);
48
+ const textWithoutCode = removeCodeBlocks(content);
49
+ return WADD_PATTERN.test(textWithoutCode);
24
50
  }
25
51
 
52
+ /**
53
+ * Extracts text from message parts array
54
+ */
26
55
  function extractPromptText(parts: any[]): string {
27
56
  if (!parts || !Array.isArray(parts)) return "";
28
57
  return parts
29
58
  .filter((p: any) => p.type === "text")
30
59
  .map((p: any) => p.text || "")
31
- .join("");
60
+ .join(" ");
32
61
  }
33
62
 
34
63
  /**
35
64
  * Hook: chat.message
36
- * Intercepts user messages to detect WADD keyword and activate SA mode
65
+ * Intercepts user messages to detect WADD keyword and activate SA mode.
66
+ * Follows oh-my-opencode pattern: inject message before user content.
37
67
  */
38
68
  export const chatMessageHook = async (input: any, output: any) => {
39
- const content = extractPromptText(output?.parts);
40
- if (!content) return;
69
+ const promptText = extractPromptText(output?.parts);
70
+ if (!promptText) return;
41
71
 
42
- if (detectWaddKeyword(content)) {
72
+ if (!detectWaddKeyword(promptText)) return;
73
+
74
+ // Set variant to "max" for best model (like ultrawork)
75
+ if (output.message) {
76
+ output.message.variant = "max";
77
+ } else {
43
78
  output.variant = "max";
44
-
45
- if (output.toasts) {
46
- output.toasts.push({
47
- type: "info",
48
- title: "SA Assistant Mode",
49
- message: "AWS Solutions Architect mode activated"
50
- });
51
- }
79
+ }
80
+
81
+ // Find text part and inject SA mode message before content
82
+ const textPartIndex = output.parts?.findIndex(
83
+ (p: any) => p.type === "text" && p.text !== undefined
84
+ );
85
+
86
+ if (textPartIndex !== -1 && output.parts[textPartIndex]) {
87
+ const originalText = output.parts[textPartIndex].text ?? "";
88
+ output.parts[textPartIndex].text = `${SA_MODE_MESSAGE}\n\n---\n\n${originalText}`;
89
+ }
90
+
91
+ // Show toast notification
92
+ if (output.toasts) {
93
+ output.toasts.push({
94
+ type: "info",
95
+ title: "SA Assistant Mode",
96
+ message: "AWS Solutions Architect mode activated"
97
+ });
52
98
  }
53
99
  };
54
100
 
55
101
  /**
56
102
  * Hook: experimental.chat.system.transform
57
- * Injects SA orchestrator system prompt when SA mode is active
103
+ * Injects SA orchestrator system prompt when SA mode is active.
58
104
  */
59
105
  export const systemTransformHook = async (input: any, output: any) => {
60
106
  const hasWadd = input.messages?.some((m: any) => {
@@ -64,7 +110,7 @@ export const systemTransformHook = async (input: any, output: any) => {
64
110
  const text = parts
65
111
  .filter((p: any) => p.type === "text")
66
112
  .map((p: any) => p.text || "")
67
- .join("");
113
+ .join(" ");
68
114
  return detectWaddKeyword(text);
69
115
  }
70
116
  return typeof parts === "string" && detectWaddKeyword(parts);
package/src/index.ts CHANGED
@@ -1,12 +1,21 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
2
  import { chatMessageHook, systemTransformHook } from "./hooks/wadd-mode";
3
- import { installSAAgents } from "./agents";
3
+ import { installSAAgents, installSASkills } from "./agents";
4
4
 
5
5
  export const SaAssistantPlugin: Plugin = async (ctx) => {
6
- const { installed, skipped } = installSAAgents();
6
+ const agentResult = installSAAgents();
7
+ const skillResult = installSASkills();
7
8
 
8
- if (installed.length > 0) {
9
- console.log(`[SA Assistant] Installed agents: ${installed.join(", ")}`);
9
+ if (agentResult.installed.length > 0) {
10
+ console.log(`[SA Assistant] Installed agents: ${agentResult.installed.join(", ")}`);
11
+ }
12
+
13
+ if (skillResult.installed.length > 0) {
14
+ console.log(`[SA Assistant] Installed skills: ${skillResult.installed.join(", ")}`);
15
+ }
16
+
17
+ if (skillResult.errors.length > 0) {
18
+ console.warn(`[SA Assistant] Skill installation errors: ${skillResult.errors.join(", ")}`);
10
19
  }
11
20
 
12
21
  return {