maskweaver 0.8.5 → 0.8.7

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.
Files changed (74) hide show
  1. package/README.ko.md +214 -186
  2. package/README.md +232 -204
  3. package/assets/agents/squad-operator.md +56 -56
  4. package/assets/commands/weave-approve-plan.md +57 -57
  5. package/assets/commands/weave-craft.md +43 -43
  6. package/assets/commands/weave-design.md +64 -64
  7. package/assets/commands/weave-flow.md +48 -48
  8. package/assets/commands/weave-help.md +101 -101
  9. package/assets/commands/weave-init.md +23 -23
  10. package/assets/commands/weave-plan.md +15 -15
  11. package/assets/commands/weave-prepare.md +69 -69
  12. package/assets/commands/weave-refine-plan.md +59 -59
  13. package/assets/commands/weave-repair.md +70 -70
  14. package/assets/commands/weave-research.md +51 -51
  15. package/assets/commands/weave-spec.md +227 -227
  16. package/assets/commands/weave-status.md +2 -2
  17. package/assets/commands/weave-verify.md +44 -44
  18. package/assets/commands/weave-worktree.md +69 -69
  19. package/dist/cli/doctor.d.ts +16 -0
  20. package/dist/cli/doctor.d.ts.map +1 -0
  21. package/dist/cli/doctor.js +355 -0
  22. package/dist/cli/doctor.js.map +1 -0
  23. package/dist/cli/install.js +9 -0
  24. package/dist/cli/install.js.map +1 -1
  25. package/dist/plugin/config/index.d.ts +26 -10
  26. package/dist/plugin/config/index.d.ts.map +1 -1
  27. package/dist/plugin/config/index.js +36 -15
  28. package/dist/plugin/config/index.js.map +1 -1
  29. package/dist/plugin/index.d.ts +1 -27
  30. package/dist/plugin/index.d.ts.map +1 -1
  31. package/dist/plugin/index.js +118 -254
  32. package/dist/plugin/index.js.map +1 -1
  33. package/dist/plugin/tools/slashcommand.js +59 -59
  34. package/dist/plugin/tools/squad.js +3 -3
  35. package/dist/plugin/tools/weave.d.ts +22 -1
  36. package/dist/plugin/tools/weave.d.ts.map +1 -1
  37. package/dist/plugin/tools/weave.js +1223 -126
  38. package/dist/plugin/tools/weave.js.map +1 -1
  39. package/dist/plugin/types.d.ts +10 -8
  40. package/dist/plugin/types.d.ts.map +1 -1
  41. package/dist/plugin/types.js +2 -0
  42. package/dist/plugin/types.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.d.ts.map +1 -1
  45. package/dist/version.js +1 -1
  46. package/dist/version.js.map +1 -1
  47. package/dist/weave/change-artifacts.d.ts +25 -0
  48. package/dist/weave/change-artifacts.d.ts.map +1 -0
  49. package/dist/weave/change-artifacts.js +184 -0
  50. package/dist/weave/change-artifacts.js.map +1 -0
  51. package/dist/weave/loop.d.ts +99 -0
  52. package/dist/weave/loop.d.ts.map +1 -0
  53. package/dist/weave/loop.js +426 -0
  54. package/dist/weave/loop.js.map +1 -0
  55. package/dist/weave/phase-manager.d.ts.map +1 -1
  56. package/dist/weave/phase-manager.js +12 -0
  57. package/dist/weave/phase-manager.js.map +1 -1
  58. package/dist/weave/stages/archive.d.ts +14 -0
  59. package/dist/weave/stages/archive.d.ts.map +1 -0
  60. package/dist/weave/stages/archive.js +38 -0
  61. package/dist/weave/stages/archive.js.map +1 -0
  62. package/dist/weave/stages/execute.d.ts.map +1 -1
  63. package/dist/weave/stages/execute.js +19 -4
  64. package/dist/weave/stages/execute.js.map +1 -1
  65. package/dist/weave/stages/plan.d.ts.map +1 -1
  66. package/dist/weave/stages/plan.js +6 -0
  67. package/dist/weave/stages/plan.js.map +1 -1
  68. package/dist/weave/types.d.ts +60 -0
  69. package/dist/weave/types.d.ts.map +1 -1
  70. package/dist/weave/worktree.d.ts.map +1 -1
  71. package/dist/weave/worktree.js +19 -0
  72. package/dist/weave/worktree.js.map +1 -1
  73. package/package.json +60 -58
  74. package/postinstall.mjs +97 -0
@@ -12,16 +12,13 @@
12
12
  * Based on oh-my-opencode plugin development patterns.
13
13
  */
14
14
  import { z } from 'zod';
15
- // Inline shim: tool() is just an identity function in @opencode-ai/plugin
16
- // We inline it to avoid bundler resolution bugs with the upstream package
17
- const tool = (input) => input;
18
15
  import * as fs from 'node:fs';
19
16
  import * as path from 'node:path';
20
17
  import * as os from 'node:os';
18
+ import { spawnSync } from 'node:child_process';
21
19
  import { fileURLToPath } from 'node:url';
22
- import { parse as parseYaml } from 'yaml';
23
20
  import { VERSION } from '../version.js';
24
- import { loadPluginConfig, isMaskEnabled, isToolEnabled, getDefaultMask, isAutoActivateEnabled, getAgentOverride, isVerboseLoggingEnabled, validateConfig, } from './config/index.js';
21
+ import { loadPluginConfig, isMaskEnabled, isToolEnabled, getDefaultMask, isAutoActivateEnabled, isVerboseLoggingEnabled, isCompletionSoundEnabled, validateConfig, } from './config/index.js';
25
22
  // New tool imports
26
23
  import { createMemorySearchTool } from './tools/memorySearch.js';
27
24
  import { createMemoryWriteTool } from './tools/memoryWrite.js';
@@ -424,8 +421,17 @@ function buildRichPrompt(mask) {
424
421
  }
425
422
  return parts.join('\n');
426
423
  }
424
+ function pluginLog(client, level, message) {
425
+ client.app.log({
426
+ body: {
427
+ service: 'maskweaver',
428
+ level,
429
+ message,
430
+ },
431
+ });
432
+ }
427
433
  // ============================================================================
428
- // Tool Factory Functions (oh-my-opencode pattern)
434
+ // Helper functions for tool factories
429
435
  // ============================================================================
430
436
  function createListMasksTool(maskLoader, activeMask) {
431
437
  return {
@@ -553,82 +559,56 @@ Active mask: ${active ? `${active.profile.name} (${active.metadata.id})` : 'none
553
559
  },
554
560
  };
555
561
  }
556
- let state = null;
557
- function parseAgentMarkdown(content) {
558
- const parts = content.split('---');
559
- if (parts.length < 3) {
560
- return { prompt: content.trim() };
561
- }
562
+ function getSessionId(event) {
563
+ if (typeof event.sessionID === 'string')
564
+ return event.sessionID;
565
+ if (typeof event.sessionId === 'string')
566
+ return event.sessionId;
567
+ return null;
568
+ }
569
+ function runSoundCommand(command, args) {
562
570
  try {
563
- const frontmatter = parseYaml(parts[1]);
564
- const prompt = parts.slice(2).join('---').trim();
565
- return { ...frontmatter, prompt };
571
+ const result = spawnSync(command, args, {
572
+ stdio: 'ignore',
573
+ windowsHide: true,
574
+ timeout: 2500,
575
+ });
576
+ return !result.error && result.status === 0;
566
577
  }
567
- catch (e) {
568
- return { prompt: content.trim() };
578
+ catch {
579
+ return false;
569
580
  }
570
581
  }
571
- // ============================================================================
572
- // Default Embedded Agents (ensures first-run works without restart)
573
- // ============================================================================
574
- const DEFAULT_AGENTS = {
575
- 'dummy-human': {
576
- description: 'Dummy-Human - Pure execution agent that performs tasks with masks assigned by Mask Weaver',
577
- mode: 'subagent',
578
- temperature: 0.2,
579
- permission: {
580
- edit: 'allow',
581
- bash: 'allow',
582
- webfetch: 'allow',
583
- },
584
- prompt: `# Dummy-Human
585
-
586
- You are a **Dummy-Human**.
587
-
588
- ## Identity
589
-
590
- You are a pure execution agent. You accurately perform work instructions received from the Mask Weaver.
591
-
592
- ## Behavior Principles
593
-
594
- 1. If the Mask Weaver provides a **mask (persona)**, become that expert and work accordingly
595
- 2. If no mask is provided, work as a competent software engineer
596
- 3. Complete assigned tasks accurately
597
- 4. Report results clearly
598
-
599
- ## Result Reporting
600
-
601
- When work is complete:
602
- - Summary of work performed
603
- - Generated outputs
604
- - Additional considerations (if any)`,
605
- },
606
- };
607
- function loadAgentAssets(...assetsDirs) {
608
- // Start with default embedded agents (always available)
609
- const agents = { ...DEFAULT_AGENTS };
610
- // Load from each directory in order (later directories override earlier ones)
611
- for (const assetsDir of assetsDirs) {
612
- const agentsDir = path.join(assetsDir, 'agents');
613
- if (!fs.existsSync(agentsDir))
614
- continue;
582
+ function playCompletionSound(config) {
583
+ if (!isCompletionSoundEnabled(config))
584
+ return;
585
+ let played = false;
586
+ if (process.platform === 'win32') {
587
+ played = runSoundCommand('powershell', [
588
+ '-NoProfile',
589
+ '-NonInteractive',
590
+ '-Command',
591
+ '[console]::beep(880,220)',
592
+ ]);
593
+ }
594
+ else if (process.platform === 'darwin') {
595
+ played = runSoundCommand('afplay', ['/System/Library/Sounds/Glass.aiff']);
596
+ }
597
+ else if (process.platform === 'linux') {
598
+ played = runSoundCommand('canberra-gtk-play', ['-i', 'complete', '-d', 'maskweaver']);
599
+ }
600
+ if (!played) {
615
601
  try {
616
- const files = fs.readdirSync(agentsDir);
617
- for (const file of files) {
618
- if (file.endsWith('.md') && file !== 'dummy-template.md') {
619
- const agentId = path.basename(file, '.md');
620
- const content = fs.readFileSync(path.join(agentsDir, file), 'utf-8');
621
- agents[agentId] = parseAgentMarkdown(content);
622
- }
623
- }
602
+ process.stdout.write('\u0007');
624
603
  }
625
- catch (e) {
626
- // Ignore errors - default agents still available
604
+ catch {
605
+ // Ignore failures - notification sound is best-effort only.
627
606
  }
628
607
  }
629
- return agents;
630
608
  }
631
- export const MaskweaverPlugin = async ({ client, directory }) => {
609
+ let state = null;
610
+ // ============================================================================
611
+ export const MaskweaverPlugin = async ({ client, directory, project, worktree, $, serverUrl }) => {
632
612
  // ==========================================================================
633
613
  // 1. Load Configuration (oh-my-opencode pattern)
634
614
  // ==========================================================================
@@ -636,11 +616,7 @@ export const MaskweaverPlugin = async ({ client, directory }) => {
636
616
  // Validate configuration
637
617
  const configErrors = validateConfig(pluginConfig);
638
618
  if (configErrors.length > 0) {
639
- client.app.log({
640
- service: 'maskweaver',
641
- level: 'warn',
642
- message: `Configuration validation errors: ${configErrors.join(', ')}`,
643
- });
619
+ pluginLog(client, 'warn', `Configuration validation errors: ${configErrors.join(', ')}`);
644
620
  }
645
621
  const verbose = isVerboseLoggingEnabled(pluginConfig);
646
622
  // ==========================================================================
@@ -650,24 +626,12 @@ export const MaskweaverPlugin = async ({ client, directory }) => {
650
626
  // Track if this is a first-time installation
651
627
  const isFirstInstall = installResult.installed.length > 0;
652
628
  if (isFirstInstall) {
653
- client.app.log({
654
- service: 'maskweaver',
655
- level: 'info',
656
- message: `Installed ${installResult.installed.length} files to .opencode/ (agents, masks)`,
657
- });
629
+ pluginLog(client, 'info', `Installed ${installResult.installed.length} files to .opencode/ (agents, masks)`);
658
630
  // Show prominent restart message for first-time installation
659
- client.app.log({
660
- service: 'maskweaver',
661
- level: 'warn',
662
- message: `⚠️ RESTART REQUIRED: Please restart OpenCode to activate all Maskweaver features (agents, masks, commands).`,
663
- });
631
+ pluginLog(client, 'warn', `⚠️ RESTART REQUIRED: Please restart OpenCode to activate all Maskweaver features (agents, masks, commands).`);
664
632
  }
665
633
  if (installResult.errors.length > 0) {
666
- client.app.log({
667
- service: 'maskweaver',
668
- level: 'warn',
669
- message: `Asset errors: ${installResult.errors.join(', ')}`,
670
- });
634
+ pluginLog(client, 'warn', `Asset errors: ${installResult.errors.join(', ')}`);
671
635
  }
672
636
  // ==========================================================================
673
637
  // 3. Initialize masks
@@ -677,37 +641,27 @@ export const MaskweaverPlugin = async ({ client, directory }) => {
677
641
  const projectMasksDir = path.join(directory, '.opencode', 'masks');
678
642
  // Priority: project masks > global masks
679
643
  const masksDir = fs.existsSync(projectMasksDir) ? projectMasksDir : globalMasksDir;
680
- state = {
644
+ const pluginState = {
681
645
  maskLoader: null,
682
646
  activeMask: null,
683
647
  masksDir,
684
648
  config: pluginConfig,
649
+ currentSessionID: null,
685
650
  };
651
+ state = pluginState;
686
652
  // Log plugin loaded
687
- client.app.log({
688
- service: 'maskweaver',
689
- level: 'info',
690
- message: `Maskweaver plugin loaded v${VERSION}`,
691
- });
653
+ pluginLog(client, 'info', `Maskweaver plugin loaded v${VERSION}`);
692
654
  if (fs.existsSync(masksDir)) {
693
- state.maskLoader = new MaskLoader(masksDir, pluginConfig);
655
+ pluginState.maskLoader = new MaskLoader(masksDir, pluginConfig);
694
656
  try {
695
- await state.maskLoader.loadCatalog();
657
+ await pluginState.maskLoader.loadCatalog();
696
658
  if (verbose) {
697
- client.app.log({
698
- service: 'maskweaver',
699
- level: 'info',
700
- message: `Masks found at: ${masksDir}`,
701
- });
659
+ pluginLog(client, 'info', `Masks found at: ${masksDir}`);
702
660
  }
703
661
  }
704
662
  catch (e) {
705
- client.app.log({
706
- service: 'maskweaver',
707
- level: 'warn',
708
- message: `Failed to load masks: ${e}`,
709
- });
710
- state.maskLoader = null;
663
+ pluginLog(client, 'warn', `Failed to load masks: ${e}`);
664
+ pluginState.maskLoader = null;
711
665
  }
712
666
  }
713
667
  // ==========================================================================
@@ -715,40 +669,27 @@ export const MaskweaverPlugin = async ({ client, directory }) => {
715
669
  // ==========================================================================
716
670
  const defaultMaskId = getDefaultMask(pluginConfig);
717
671
  const autoActivate = isAutoActivateEnabled(pluginConfig);
718
- if (defaultMaskId && autoActivate && state.maskLoader) {
672
+ if (defaultMaskId && autoActivate && pluginState.maskLoader) {
719
673
  try {
720
- const defaultMask = await state.maskLoader.load(defaultMaskId);
674
+ const defaultMask = await pluginState.maskLoader.load(defaultMaskId);
721
675
  if (defaultMask) {
722
- state.activeMask = defaultMask;
723
- client.app.log({
724
- service: 'maskweaver',
725
- level: 'info',
726
- message: `Auto-activated default mask: ${defaultMaskId} (${defaultMask.profile.name})`,
727
- });
676
+ pluginState.activeMask = defaultMask;
677
+ pluginLog(client, 'info', `Auto-activated default mask: ${defaultMaskId} (${defaultMask.profile.name})`);
728
678
  }
729
679
  else {
730
- client.app.log({
731
- service: 'maskweaver',
732
- level: 'warn',
733
- message: `Default mask "${defaultMaskId}" not found or disabled`,
734
- });
680
+ pluginLog(client, 'warn', `Default mask "${defaultMaskId}" not found or disabled`);
735
681
  }
736
682
  }
737
683
  catch (e) {
738
- client.app.log({
739
- service: 'maskweaver',
740
- level: 'warn',
741
- message: `Failed to auto-activate default mask: ${e}`,
742
- });
684
+ pluginLog(client, 'warn', `Failed to auto-activate default mask: ${e}`);
743
685
  }
744
686
  }
745
687
  // ==========================================================================
746
688
  // 5. Helper functions for tool factories
747
689
  // ==========================================================================
748
- const getActiveMask = () => state?.activeMask || null;
690
+ const getActiveMask = () => pluginState.activeMask;
749
691
  const setActiveMask = (mask) => {
750
- if (state)
751
- state.activeMask = mask;
692
+ pluginState.activeMask = mask;
752
693
  };
753
694
  // ==========================================================================
754
695
  // 6. Conditional tool registration (oh-my-opencode pattern)
@@ -756,28 +697,29 @@ export const MaskweaverPlugin = async ({ client, directory }) => {
756
697
  const isToolActive = (toolName) => isToolEnabled(pluginConfig, toolName);
757
698
  // Helper to ensure tool arguments are compatible with opencode's expected format.
758
699
  // opencode expects a ZodRawShape (raw object), NOT a ZodObject instance.
700
+ // Zod 4: schema.def.shape, Zod 3: schema._def.shape()
759
701
  const wrapSchema = (schema) => {
760
702
  if (!schema || typeof schema !== 'object')
761
703
  return schema;
762
- // If it's a ZodObject (Zod 4), extract its shape
763
- if (schema.def && typeof schema.def === 'object' && schema.type === 'object') {
704
+ // Zod 4 def.shape is a plain object
705
+ if (schema.def && typeof schema.def === 'object' && schema.type === 'object' && schema.def.shape && typeof schema.def.shape === 'object') {
764
706
  return schema.def.shape;
765
707
  }
766
- // If it's a ZodObject (Zod 3), extract its shape
708
+ // Zod 3 _def.shape() returns a plain object
767
709
  if (schema._def && typeof schema._def.shape === 'function') {
768
710
  return schema._def.shape();
769
711
  }
770
712
  return schema;
771
713
  };
772
714
  const tools = {};
773
- if (state.maskLoader) {
715
+ if (pluginState.maskLoader) {
774
716
  if (isToolActive('list_masks')) {
775
- const tool = createListMasksTool(state.maskLoader, getActiveMask);
717
+ const tool = createListMasksTool(pluginState.maskLoader, getActiveMask);
776
718
  tool.args = wrapSchema(tool.args);
777
719
  tools.list_masks = tool;
778
720
  }
779
721
  if (isToolActive('select_mask')) {
780
- const tool = createSelectMaskTool(state.maskLoader, getActiveMask, setActiveMask);
722
+ const tool = createSelectMaskTool(pluginState.maskLoader, getActiveMask, setActiveMask);
781
723
  tool.args = wrapSchema(tool.args);
782
724
  tools.select_mask = tool;
783
725
  }
@@ -787,13 +729,13 @@ export const MaskweaverPlugin = async ({ client, directory }) => {
787
729
  tools.deselect_mask = tool;
788
730
  }
789
731
  if (isToolActive('get_mask_prompt')) {
790
- const tool = createGetMaskPromptTool(state.maskLoader, getActiveMask);
732
+ const tool = createGetMaskPromptTool(pluginState.maskLoader, getActiveMask);
791
733
  tool.args = wrapSchema(tool.args);
792
734
  tools.get_mask_prompt = tool;
793
735
  }
794
736
  }
795
737
  if (isToolActive('maskweaver_status')) {
796
- const tool = createMaskweaverStatusTool(state.maskLoader, masksDir, getActiveMask);
738
+ const tool = createMaskweaverStatusTool(pluginState.maskLoader, masksDir, getActiveMask);
797
739
  tool.args = wrapSchema(tool.args);
798
740
  tools.maskweaver_status = tool;
799
741
  }
@@ -885,104 +827,20 @@ export const MaskweaverPlugin = async ({ client, directory }) => {
885
827
  };
886
828
  }
887
829
  // ==========================================================================
888
- // 8. Load and register agents
830
+ // 8. Agents are loaded from .opencode/agents/*.md files by OpenCode's
831
+ // filesystem-based agent loader (see config/agent.ts:110-140).
832
+ // installAssets() in step 2 copies agent .md files so they are picked up.
833
+ // The 'agent' property in Hooks is NOT consumed by OpenCode — confirmed
834
+ // by source analysis (packages/opencode/src/plugin/index.ts:92-103).
889
835
  // ==========================================================================
890
- const assetsDir = getAssetsDir();
891
- const projectOpencodeDir = path.join(directory, '.opencode');
892
- // Load from package assets first, then project .opencode (project overrides package)
893
- const loadedAgents = loadAgentAssets(assetsDir, projectOpencodeDir);
894
836
  // ==========================================================================
895
- // 8b. Generate agents from model pool (or legacy fallback)
896
- // ==========================================================================
897
- {
898
- const { loadRuntimeConfig, normalizeDummyHumansConfig } = await import('../shared/config.js');
899
- const { createModelRegistry } = await import('../shared/model-registry.js');
900
- const runtimeConfig = loadRuntimeConfig(directory);
901
- if (runtimeConfig.dummyHumans) {
902
- const pool = normalizeDummyHumansConfig(runtimeConfig.dummyHumans);
903
- // Initialize the global model registry
904
- createModelRegistry(pool);
905
- if (loadedAgents['dummy-human']) {
906
- // Generate agent variants from pool entries
907
- for (const entry of pool) {
908
- const agentName = `dummy-${entry.id}`;
909
- if (!loadedAgents[agentName]) {
910
- const tierLabel = entry.tier === 'flash' ? 'Flash' : entry.tier === 'premium' ? 'Premium' : 'Standard';
911
- const capStr = entry.capabilities.slice(0, 3).join(', ');
912
- loadedAgents[agentName] = {
913
- ...loadedAgents['dummy-human'],
914
- description: `Dummy-Human (${entry.id}) - ${tierLabel}. ${entry.description || capStr}. [max ${entry.maxConcurrent} concurrent]`,
915
- model: entry.model,
916
- };
917
- }
918
- }
919
- // Also ensure legacy dummy-flash / dummy-premium aliases exist (for backward compat)
920
- const flashEntry = pool.find(e => e.tier === 'flash');
921
- const humanEntry = pool.find(e => e.tier === 'human');
922
- const premiumEntry = pool.find(e => e.tier === 'premium');
923
- if (flashEntry && !loadedAgents['dummy-flash']) {
924
- loadedAgents['dummy-flash'] = loadedAgents[`dummy-${flashEntry.id}`];
925
- }
926
- if (humanEntry) {
927
- // dummy-human already exists as the base agent; just update its model from pool
928
- loadedAgents['dummy-human'].model = humanEntry.model;
929
- }
930
- if (premiumEntry && !loadedAgents['dummy-premium']) {
931
- loadedAgents['dummy-premium'] = loadedAgents[`dummy-${premiumEntry.id}`];
932
- }
933
- // Fallback: if no tier mapping found, use defaults
934
- if (!loadedAgents['dummy-flash']) {
935
- loadedAgents['dummy-flash'] = {
936
- ...loadedAgents['dummy-human'],
937
- description: 'Dummy-Human (Flash) - Fast and cheap',
938
- model: 'google/gemini-2.0-flash',
939
- };
940
- }
941
- if (!loadedAgents['dummy-premium']) {
942
- loadedAgents['dummy-premium'] = {
943
- ...loadedAgents['dummy-human'],
944
- description: 'Dummy-Human (Premium) - Powerful and reasoning',
945
- model: 'google/gemini-2.0-pro-exp-02-05',
946
- };
947
- }
948
- }
949
- }
950
- else {
951
- // No pool config → legacy hardcoded defaults
952
- if (loadedAgents['dummy-human']) {
953
- if (!loadedAgents['dummy-flash']) {
954
- loadedAgents['dummy-flash'] = {
955
- ...loadedAgents['dummy-human'],
956
- description: 'Dummy-Human (Flash) - Fast and cheap',
957
- model: 'google/gemini-2.0-flash',
958
- };
959
- }
960
- if (!loadedAgents['dummy-premium']) {
961
- loadedAgents['dummy-premium'] = {
962
- ...loadedAgents['dummy-human'],
963
- description: 'Dummy-Human (Premium) - Powerful and reasoning',
964
- model: 'google/gemini-2.0-pro-exp-02-05',
965
- };
966
- }
967
- }
968
- }
969
- }
970
- // Apply config overrides to agents
971
- for (const agentId of Object.keys(loadedAgents)) {
972
- const override = getAgentOverride(pluginConfig, agentId);
973
- if (override) {
974
- if (override.model)
975
- loadedAgents[agentId].model = override.model;
976
- if (override.systemPrompt)
977
- loadedAgents[agentId].prompt = override.systemPrompt;
978
- }
979
- }
980
- // ==========================================================================
981
- // 9. Return plugin hooks
837
+ // 9. Return plugin hooks (official OpenCode Hooks interface only)
838
+ // Note: Agents are registered via .opencode/agents/*.md files (installed by
839
+ // installAssets()), NOT via the plugin return. The Hooks type does not
840
+ // include 'agent' OpenCode loads agents from the filesystem exclusively.
982
841
  // ==========================================================================
983
842
  return {
984
- // Agent registration
985
- agent: loadedAgents,
843
+ // Agent registration handled via .opencode/agents/*.md files (see installAssets)
986
844
  // System prompt transform - inject active mask
987
845
  'experimental.chat.system.transform': async (_input, output) => {
988
846
  if (state?.activeMask) {
@@ -1000,40 +858,46 @@ ${buildRichPrompt(state.activeMask)}
1000
858
  event: async ({ event }) => {
1001
859
  // Session created - log available masks
1002
860
  if (event.type === 'session.created') {
1003
- if (state?.maskLoader && verbose) {
861
+ pluginState.currentSessionID = getSessionId(event);
862
+ if (pluginState.maskLoader && verbose) {
1004
863
  try {
1005
- const masks = await state.maskLoader.listAll();
1006
- const categories = await state.maskLoader.listCategories();
1007
- client.app.log({
1008
- service: 'maskweaver',
1009
- level: 'info',
1010
- message: `Session started - ${masks.length} masks available across ${categories.length} categories`,
1011
- });
864
+ const masks = await pluginState.maskLoader.listAll();
865
+ const categories = await pluginState.maskLoader.listCategories();
866
+ pluginLog(client, 'info', `Session started - ${masks.length} masks available across ${categories.length} categories`);
1012
867
  }
1013
868
  catch (_e) {
1014
869
  // Ignore errors
1015
870
  }
1016
871
  }
1017
872
  }
873
+ // Session idle - generation completed
874
+ if (event.type === 'session.idle') {
875
+ const idleSessionID = getSessionId(event);
876
+ const isCurrentSession = !idleSessionID ||
877
+ !pluginState.currentSessionID ||
878
+ idleSessionID === pluginState.currentSessionID;
879
+ if (isCurrentSession) {
880
+ playCompletionSound(pluginState.config);
881
+ }
882
+ }
1018
883
  // Session deleted - cleanup
1019
884
  if (event.type === 'session.deleted') {
1020
- if (state && verbose) {
1021
- const wasActive = state.activeMask !== null;
1022
- state.activeMask = null;
885
+ const deletedSessionID = getSessionId(event);
886
+ if (!deletedSessionID || deletedSessionID === pluginState.currentSessionID) {
887
+ pluginState.currentSessionID = null;
888
+ }
889
+ if (verbose) {
890
+ const wasActive = pluginState.activeMask !== null;
891
+ pluginState.activeMask = null;
1023
892
  if (wasActive) {
1024
- client.app.log({
1025
- service: 'maskweaver',
1026
- level: 'info',
1027
- message: 'Session ended - active mask cleared',
1028
- });
893
+ pluginLog(client, 'info', 'Session ended - active mask cleared');
1029
894
  }
1030
895
  }
1031
896
  }
1032
897
  },
1033
- // Config hook - (oh-my-opencode pattern)
898
+ // Config hook - allows plugins to modify opencode configuration
1034
899
  config: async (config) => {
1035
- // NOTE: Current opencode version expects config to be a function, not an object.
1036
- // Agent overrides are currently not supported via this hook in opencode core.
900
+ // Reserved for future configuration injection (model, provider, etc.)
1037
901
  return;
1038
902
  },
1039
903
  };