@aexol/opencode-wizard 0.3.4 → 0.3.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.
Files changed (38) hide show
  1. package/README.md +9 -7
  2. package/dist/graphql-operations.d.ts +5 -2
  3. package/dist/graphql-operations.js +161 -156
  4. package/dist/graphql-operations.js.map +1 -1
  5. package/dist/plugin-tools.d.ts +26 -0
  6. package/dist/plugin-tools.js +43 -7
  7. package/dist/plugin-tools.js.map +1 -1
  8. package/dist/published-skills-system-note.js +3 -7
  9. package/dist/published-skills-system-note.js.map +1 -1
  10. package/dist/published-skills-terminology.d.ts +21 -0
  11. package/dist/published-skills-terminology.js +38 -0
  12. package/dist/published-skills-terminology.js.map +1 -0
  13. package/dist/published-skills-transform.d.ts +99 -2
  14. package/dist/published-skills-transform.js +91 -19
  15. package/dist/published-skills-transform.js.map +1 -1
  16. package/dist/server/auth-bootstrap.d.ts +7 -0
  17. package/dist/server/auth-bootstrap.js +89 -0
  18. package/dist/server/auth-bootstrap.js.map +1 -0
  19. package/dist/server/client.d.ts +30 -1
  20. package/dist/server/client.js +81 -1
  21. package/dist/server/client.js.map +1 -1
  22. package/dist/server/preferences.d.ts +22 -0
  23. package/dist/server/preferences.js +121 -0
  24. package/dist/server/preferences.js.map +1 -0
  25. package/dist/server/runtime.d.ts +3 -22
  26. package/dist/server/runtime.js +447 -242
  27. package/dist/server/runtime.js.map +1 -1
  28. package/dist/server/types.d.ts +75 -0
  29. package/dist/server/types.js.map +1 -1
  30. package/dist/smoke-published-skills.js +4 -4
  31. package/dist/smoke-published-skills.js.map +1 -1
  32. package/dist/tui/components/skill-catalog-row.js +45 -44
  33. package/dist/tui/components/skill-catalog-row.js.map +1 -1
  34. package/dist/tui/components/wizard-skills-dialog-content.js +73 -63
  35. package/dist/tui/components/wizard-skills-dialog-content.js.map +1 -1
  36. package/dist/tui/skill-helpers.js +7 -6
  37. package/dist/tui/skill-helpers.js.map +1 -1
  38. package/package.json +1 -1
@@ -1,226 +1,97 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { CREATE_OR_UPDATE_SKILL_FROM_MARKDOWN_MUTATION, SET_PUBLISHED_SKILL_PREFERENCE_MUTATION } from '../graphql-operations.js';
4
- import { createPublishedSkillToolDefinitions } from '../plugin-tools.js';
5
- import { readGlobalAuthState, resolveStoredAuthState, toAuthState, toStoredUserKey, writeAuthState } from './auth-store.js';
3
+ import { CREATE_OR_UPDATE_SKILL_FROM_MARKDOWN_MUTATION, SET_WIZARD_ARTIFACT_PREFERENCE_MUTATION } from '../graphql-operations.js';
4
+ import { createPublishedSkillToolDefinitions, resolveAvailableTools } from '../plugin-tools.js';
5
+ import { resolveStoredAuthState, toAuthState, writeAuthState } from './auth-store.js';
6
6
  import { resolveConfig } from './config.js';
7
7
  export { resolveConfig } from './config.js';
8
8
  import { createPluginSession, openBrowser, startLoginFlow } from './auth-flow.js';
9
- import { fetchPublishedSkillDetail, fetchPublishedSkillsCatalog, fetchPublishedSkillsGraphQl, maybePersistWorkspaceSlugFromCatalog } from './client.js';
9
+ import { fetchPublishedSkillDetail, fetchPublishedSkillsCatalog, fetchPublishedSkillsGraphQl, fetchWizardArtifactDetail, fetchWizardArtifactsCatalog, maybePersistWorkspaceSlugFromCatalog } from './client.js';
10
10
  import { normalizeAbsolutePath } from './path-utils.js';
11
11
  import { emitPluginActionEvent, emitPresenceEvent } from './presence.js';
12
- import { normalizeDirectoryArg, normalizeRepositoryPath, resolveWorkspace, toDeliveryInput, toWorkspaceResolutionMetadata, toWorkspaceResolutionOutput } from './workspace.js';
13
- import { parseRequestedSkillArgs, selectPublishedSkills, toPublishedSkillDetail, toPublishedSkillSummary } from '../published-skills-transform.js';
14
- import { CACHE_TTL_MS, LOGIN_TIMEOUT_MS, OIDC_CALLBACK_URL, PLUGIN_ID, PRESENCE_EVENT_TIMEOUT_MS, PRESENCE_SHUTDOWN_SIGNALS, PRESENCE_SIGNAL_EXIT_CODES } from './constants.js';
12
+ import { normalizeDirectoryArg, normalizeRepositoryPath, resolveWorkspace, toWorkspaceResolutionMetadata, toWorkspaceResolutionOutput } from './workspace.js';
13
+ import { parseRequestedSkillArgs, selectPublishedSkills, toPublishedSkillDetail, toPublishedSkillSummary, toWizardArtifactCatalog, toWizardArtifactDetail, toWizardArtifactSummary } from '../published-skills-transform.js';
14
+ import { CACHE_TTL_MS, LOGIN_TIMEOUT_MS, OIDC_CALLBACK_URL, PLUGIN_ID, PRESENCE_SHUTDOWN_SIGNALS, PRESENCE_SIGNAL_EXIT_CODES } from './constants.js';
15
15
  export { PLUGIN_ID, NATIVE_SKILLS_URL_COMPATIBILITY } from './constants.js';
16
- import { buildSystemNote, filterIgnoredPublishedSkills, resolvePluginStatusSnapshot, toAiFacingPluginStatusSnapshot, toFetchFailureOutput, toPluginStatusMetadata, toPublishedSkillCatalog, withStatusMessage } from './status.js';
16
+ import { buildSystemNote, filterIgnoredPublishedSkills, resolvePluginStatusSnapshot, toAiFacingPluginStatusSnapshot, toFetchFailureOutput, toPluginStatusMetadata, toPublishedSkillCatalog } from './status.js';
17
+ import { createIdleLoginBootstrapSnapshot } from './auth-bootstrap.js';
18
+ import { getCatalogCacheKey, resolvePublishedSkillPreferenceCacheContext, setPublishedSkillIgnored, setPublishedSkillInstalled, toPublishedSkillPreferenceAction, toPublishedSkillPreferenceScope } from './preferences.js';
17
19
  export { buildSystemNote, resolvePluginStatusSnapshot, toPluginAuthStateSummary, toPublishedSkillCatalog } from './status.js';
18
- const createIdleLoginBootstrapSnapshot = () => ({
19
- status: 'idle',
20
- trigger: null,
21
- startedAt: null,
22
- expiresAt: null,
23
- browserUrl: null,
24
- browserOpenError: null,
25
- email: null,
26
- message: null
27
- });
28
- const STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS = 60_000;
29
- const statusPathLoginBootstrap = {
30
- promise: null,
31
- status: 'idle',
32
- message: null,
33
- failedAt: null
34
- };
35
20
  const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
36
- let publishedSkillPreferenceCacheVersion = 0;
37
- const toIgnoredSkillSlug = value => {
38
- const normalized = value.trim().toLowerCase();
39
- if (!normalized) return null;
40
- return normalized;
41
- };
42
- const toPublishedSkillPreferenceAction = value => {
43
- const normalized = value.trim().toLowerCase();
44
- if (normalized === 'install' || normalized === 'uninstall' || normalized === 'ignore' || normalized === 'unignore') {
45
- return normalized;
46
- }
47
- throw new Error('Published skill preference action must be one of: install, uninstall, ignore, unignore.');
48
- };
49
- const toPublishedSkillPreferenceScope = (value, defaultScope) => {
50
- if (!value) return defaultScope;
51
- const normalized = value.trim().toLowerCase();
52
- if (normalized === 'global' || normalized === 'project') return normalized;
53
- throw new Error('Published skill preferenceScope must be either global or project.');
21
+ export { resolvePluginStatusSnapshotWithAuthBootstrap } from './auth-bootstrap.js';
22
+ export { setPublishedSkillIgnored, setPublishedSkillInstalled } from './preferences.js';
23
+ const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
24
+ return JSON.stringify([catalogCacheKey, skillVersionId]);
54
25
  };
55
- const resolvePublishedSkillPreferenceCacheContext = async config => {
56
- const authState = await readGlobalAuthState(config.authStatePath);
57
- return {
58
- userKey: toStoredUserKey(authState),
59
- preferenceVersion: publishedSkillPreferenceCacheVersion
60
- };
26
+ const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
27
+ return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
61
28
  };
62
- const getCatalogCacheKey = (workspaceResolution, preferenceContext) => {
63
- return JSON.stringify([workspaceResolution.cacheKey, preferenceContext.userKey, preferenceContext.preferenceVersion]);
29
+ const SUPPORTED_WIZARD_ARTIFACT_KINDS = ['SKILL', 'DESIGN_DOC'];
30
+ const DESIGN_DOC_UNSUPPORTED_MESSAGE = 'DESIGN_DOC is supported by generic wizard artifact persistence; use catalog/detail tools for assigned or installed documents.';
31
+ const toWizardArtifactKind = value => {
32
+ if (!value) return 'SKILL';
33
+ const normalized = value.trim().toUpperCase().replace(/[-\s]+/gu, '_');
34
+ if (normalized === 'SKILL') return 'SKILL';
35
+ if (normalized === 'DESIGN_DOC') return 'DESIGN_DOC';
36
+ return null;
64
37
  };
65
- const startStatusPathLoginBootstrap = (worktree, config) => {
66
- if (statusPathLoginBootstrap.promise) return;
67
- if (statusPathLoginBootstrap.status === 'failed' && statusPathLoginBootstrap.failedAt && Date.now() - statusPathLoginBootstrap.failedAt < STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS) {
68
- return;
38
+ const buildUnsupportedWizardArtifactOutput = ({
39
+ artifactKind,
40
+ directoryPath
41
+ }) => ({
42
+ output: JSON.stringify({
43
+ pluginId: PLUGIN_ID,
44
+ runtimeMode: 'tool_fetch_only',
45
+ status: artifactKind === 'DESIGN_DOC' ? 'unsupported' : 'bad_artifact_kind',
46
+ artifactKind,
47
+ supportedArtifactKinds: SUPPORTED_WIZARD_ARTIFACT_KINDS,
48
+ requestedDirectoryPath: directoryPath,
49
+ message: artifactKind === 'DESIGN_DOC' ? DESIGN_DOC_UNSUPPORTED_MESSAGE : 'Unsupported wizard artifact kind. Use SKILL or DESIGN_DOC.'
50
+ }, null, 2),
51
+ metadata: {
52
+ status: artifactKind === 'DESIGN_DOC' ? 'unsupported' : 'bad_artifact_kind',
53
+ artifactKind,
54
+ directoryPath
69
55
  }
70
- statusPathLoginBootstrap.status = 'pending';
71
- statusPathLoginBootstrap.message = 'Browser login started automatically from the TUI/status path.';
72
- statusPathLoginBootstrap.failedAt = null;
73
- statusPathLoginBootstrap.promise = (async () => {
74
- const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
75
- const loginStart = await startLoginFlow(loginSignal);
76
- const browserOpenError = await openBrowser(loginStart.browserUrl);
77
- if (browserOpenError) {
78
- statusPathLoginBootstrap.message = `Automatic browser open failed. Open ${loginStart.browserUrl} manually.`;
79
- }
80
- try {
81
- const callbackPayload = await loginStart.callbackPromise;
82
- if (callbackPayload.status === 'error') {
83
- throw new Error(callbackPayload.message);
84
- }
85
- if (callbackPayload.state !== loginStart.expectedState) {
86
- throw new Error('OAuth callback state did not match the original login request.');
56
+ });
57
+ const withWizardArtifactEnvelope = (result, artifactKind) => {
58
+ if (!result || typeof result !== 'object' || !('output' in result)) return result;
59
+ const outputResult = result;
60
+ if (typeof outputResult.output !== 'string') return result;
61
+ try {
62
+ return {
63
+ ...outputResult,
64
+ output: JSON.stringify({
65
+ ...JSON.parse(outputResult.output),
66
+ artifactKind,
67
+ supportedArtifactKinds: SUPPORTED_WIZARD_ARTIFACT_KINDS,
68
+ compatibilityAliases: ['opencode_wizard_published_skills_fetch', 'opencode_wizard_published_skill_preference_set']
69
+ }, null, 2),
70
+ metadata: {
71
+ ...(outputResult.metadata ?? {}),
72
+ artifactKind
87
73
  }
88
- const pluginSession = await createPluginSession({
89
- code: callbackPayload.code,
90
- codeVerifier: loginStart.codeVerifier,
91
- redirectUri: OIDC_CALLBACK_URL,
92
- config,
93
- signal: loginSignal
94
- });
95
- const authState = toAuthState(pluginSession);
96
- await writeAuthState(config.authStatePath, authState);
97
- statusPathLoginBootstrap.status = 'authenticated';
98
- statusPathLoginBootstrap.message = `Browser login completed successfully for ${authState.email}.`;
99
- return authState;
100
- } finally {
101
- await loginStart.closeCallbackServer().catch(() => undefined);
102
- }
103
- })().catch(error => {
104
- statusPathLoginBootstrap.status = 'failed';
105
- statusPathLoginBootstrap.failedAt = Date.now();
106
- statusPathLoginBootstrap.message = error instanceof Error ? error.message : 'Browser login failed.';
107
- throw error;
108
- }).finally(() => {
109
- statusPathLoginBootstrap.promise = null;
110
- });
111
- statusPathLoginBootstrap.promise.catch(() => undefined);
112
- };
113
- export const resolvePluginStatusSnapshotWithAuthBootstrap = async ({
114
- worktree,
115
- directory,
116
- signal
117
- }) => {
118
- const snapshot = await resolvePluginStatusSnapshot({
119
- worktree,
120
- directory,
121
- signal
122
- });
123
- if (snapshot.status !== 'missing_auth') return snapshot;
124
- const config = await resolveConfig(worktree);
125
- startStatusPathLoginBootstrap(worktree, config);
126
- if (statusPathLoginBootstrap.message) {
127
- return withStatusMessage(snapshot, statusPathLoginBootstrap.message);
74
+ };
75
+ } catch {
76
+ return result;
128
77
  }
129
- return withStatusMessage(snapshot, 'Browser login is pending from the TUI/status path.');
130
78
  };
131
- const toBackendPreferenceScope = preferenceScope => {
132
- if (preferenceScope === 'global') return 'GLOBAL';
133
- return 'WORKSPACE';
79
+ const matchesWizardArtifactIdentifier = (item, identifier) => {
80
+ const normalizedIdentifier = identifier.trim().toLowerCase();
81
+ if (!normalizedIdentifier) return false;
82
+ if (item.artifact.slug.toLowerCase() === normalizedIdentifier) return true;
83
+ if (item.artifact.name.toLowerCase() === normalizedIdentifier) return true;
84
+ if (item.artifactVersion.frontmatterName.toLowerCase() === normalizedIdentifier) return true;
85
+ return item.artifactVersion.id.toLowerCase() === normalizedIdentifier;
134
86
  };
135
- const setPublishedSkillPreference = async ({
136
- worktree,
137
- directory,
138
- config,
139
- skillSlug,
140
- preferenceScope,
141
- installed,
142
- ignored
143
- }) => {
144
- const workspaceResolution = await resolveWorkspace({
145
- config,
146
- directory
147
- });
148
- const response = await fetchPublishedSkillsGraphQl({
149
- worktree,
150
- config,
151
- query: SET_PUBLISHED_SKILL_PREFERENCE_MUTATION,
152
- variables: {
153
- input: {
154
- ...toDeliveryInput(workspaceResolution),
155
- skillSlug,
156
- preferenceScope: toBackendPreferenceScope(preferenceScope),
157
- installed,
158
- ignored
159
- }
160
- },
161
- signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
162
- });
163
- if (!response.ok) {
164
- throw new Error(response.result.message);
165
- }
166
- const preferences = response.data.setPublishedSkillPreference;
167
- publishedSkillPreferenceCacheVersion += 1;
87
+ const selectWizardArtifacts = (items, identifiers) => {
88
+ const selectedItems = items.filter(item => identifiers.some(identifier => matchesWizardArtifactIdentifier(item, identifier)));
89
+ const missingIdentifiers = identifiers.filter(identifier => !selectedItems.some(item => matchesWizardArtifactIdentifier(item, identifier)));
168
90
  return {
169
- scopeKey: preferences.scopeKey,
170
- userKey: preferences.userKey,
171
- ignoredSkillSlugs: preferences.ignoredSkills.map(item => item.skill.slug),
172
- installedGlobalSkillSlugs: [],
173
- installedWorkspaceSkillSlugs: []
91
+ selectedItems,
92
+ missingIdentifiers
174
93
  };
175
94
  };
176
- export const setPublishedSkillIgnored = async ({
177
- worktree,
178
- directory,
179
- skillSlug,
180
- ignored,
181
- preferenceScope
182
- }) => {
183
- const config = await resolveConfig(worktree);
184
- const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
185
- if (!normalizedSkillSlug) {
186
- throw new Error('Cannot toggle an empty published skill slug.');
187
- }
188
- return setPublishedSkillPreference({
189
- worktree,
190
- directory,
191
- config,
192
- skillSlug: normalizedSkillSlug,
193
- preferenceScope: preferenceScope ?? 'project',
194
- ignored
195
- });
196
- };
197
- export const setPublishedSkillInstalled = async ({
198
- worktree,
199
- directory,
200
- skillSlug,
201
- installed,
202
- preferenceScope
203
- }) => {
204
- const config = await resolveConfig(worktree);
205
- const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
206
- if (!normalizedSkillSlug) {
207
- throw new Error('Cannot toggle an empty published skill slug.');
208
- }
209
- return setPublishedSkillPreference({
210
- worktree,
211
- directory,
212
- config,
213
- skillSlug: normalizedSkillSlug,
214
- preferenceScope,
215
- installed
216
- });
217
- };
218
- const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
219
- return JSON.stringify([catalogCacheKey, skillVersionId]);
220
- };
221
- const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
222
- return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
223
- };
224
95
  export const OpencodeWizardSkillsPlugin = async input => {
225
96
  const {
226
97
  tool
@@ -561,20 +432,6 @@ export const OpencodeWizardSkillsPlugin = async input => {
561
432
  args,
562
433
  context
563
434
  }) => {
564
- const authState = await resolveStoredAuthState(input.worktree, config);
565
- if (!authState || authState.role !== 'ADMIN' && authState.role !== 'EDITOR') {
566
- return {
567
- output: JSON.stringify({
568
- pluginId: PLUGIN_ID,
569
- status: 'forbidden',
570
- message: 'This tool requires ADMIN or EDITOR role. Your current session does not have the required permission.'
571
- }, null, 2),
572
- metadata: {
573
- status: 'forbidden',
574
- role: authState?.role ?? 'none'
575
- }
576
- };
577
- }
578
435
  const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
579
436
  const requestedSkills = parseRequestedSkillArgs(args);
580
437
  const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
@@ -632,7 +489,11 @@ export const OpencodeWizardSkillsPlugin = async input => {
632
489
  const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
633
490
  const isSingleRequest = requestedSkills.length === 1;
634
491
  if (requestedSkills.length === 0) {
635
- const catalog = toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload);
492
+ const authState = await resolveStoredAuthState(input.worktree, config);
493
+ const catalog = {
494
+ ...toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload),
495
+ availableTools: resolveAvailableTools(authState?.role ?? null)
496
+ };
636
497
  context.metadata({
637
498
  title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount} active`,
638
499
  metadata: {
@@ -789,24 +650,241 @@ export const OpencodeWizardSkillsPlugin = async input => {
789
650
  }
790
651
  };
791
652
  };
792
- const executeStatusTool = async ({
653
+ const executeWizardArtifactCatalogTool = async ({
793
654
  args,
794
655
  context
795
656
  }) => {
796
- const authState = await resolveStoredAuthState(input.worktree, config);
797
- if (!authState || authState.role !== 'ADMIN' && authState.role !== 'EDITOR') {
657
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
658
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
659
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
660
+ if (!artifactKind) {
661
+ return buildUnsupportedWizardArtifactOutput({
662
+ artifactKind: args.artifactKind ?? '',
663
+ directoryPath
664
+ });
665
+ }
666
+ if (artifactKind === 'DESIGN_DOC') {
667
+ const workspaceResolution = await resolveWorkspace({
668
+ config,
669
+ directory: requestedDirectory
670
+ });
671
+ const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, clearPublishedSkillState);
672
+ if (!fetchResult.ok) {
673
+ return {
674
+ output: JSON.stringify({
675
+ pluginId: PLUGIN_ID,
676
+ runtimeMode: 'tool_fetch_only',
677
+ status: fetchResult.status,
678
+ artifactKind,
679
+ requestedDirectoryPath: directoryPath,
680
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
681
+ message: fetchResult.message,
682
+ fetchedAt: fetchResult.fetchedAt,
683
+ source: fetchResult.source
684
+ }, null, 2),
685
+ metadata: {
686
+ status: fetchResult.status,
687
+ artifactKind,
688
+ directoryPath,
689
+ source: fetchResult.source
690
+ }
691
+ };
692
+ }
693
+ const authState = await resolveStoredAuthState(input.worktree, config);
694
+ const catalog = toWizardArtifactCatalog(fetchResult.payload, {
695
+ pluginId: PLUGIN_ID,
696
+ availableTools: resolveAvailableTools(authState?.role ?? null)
697
+ });
798
698
  return {
799
699
  output: JSON.stringify({
700
+ ...catalog,
701
+ status: 'ready',
702
+ requestedDirectoryPath: directoryPath,
703
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
704
+ fetchedAt: fetchResult.fetchedAt,
705
+ source: fetchResult.source,
706
+ message: 'DESIGN_DOC catalog discovery only. Full DESIGN.md bodies/files require opencode_wizard_artifact_fetch with artifactKind DESIGN_DOC.'
707
+ }, null, 2),
708
+ metadata: {
709
+ status: 'ready',
710
+ artifactKind,
711
+ artifactCount: catalog.artifactCount.toString(),
712
+ ...toWorkspaceResolutionMetadata(workspaceResolution),
713
+ source: fetchResult.source
714
+ }
715
+ };
716
+ }
717
+ const result = await executePublishedSkillsFetchTool({
718
+ args: {
719
+ directory: args.directory,
720
+ refresh: args.refresh
721
+ },
722
+ context
723
+ });
724
+ return withWizardArtifactEnvelope(result, artifactKind);
725
+ };
726
+ const executeWizardArtifactFetchTool = async ({
727
+ args,
728
+ context
729
+ }) => {
730
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
731
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
732
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
733
+ if (!artifactKind) {
734
+ return buildUnsupportedWizardArtifactOutput({
735
+ artifactKind: args.artifactKind ?? '',
736
+ directoryPath
737
+ });
738
+ }
739
+ if (artifactKind === 'DESIGN_DOC') {
740
+ const requestedArtifacts = parseRequestedSkillArgs({
741
+ skill: args.artifact,
742
+ skills: args.artifacts
743
+ });
744
+ const workspaceResolution = await resolveWorkspace({
745
+ config,
746
+ directory: requestedDirectory
747
+ });
748
+ const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, clearPublishedSkillState);
749
+ if (!fetchResult.ok) {
750
+ return {
751
+ output: JSON.stringify({
752
+ pluginId: PLUGIN_ID,
753
+ runtimeMode: 'tool_fetch_only',
754
+ status: fetchResult.status,
755
+ artifactKind,
756
+ requestedDirectoryPath: directoryPath,
757
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
758
+ message: fetchResult.message,
759
+ fetchedAt: fetchResult.fetchedAt,
760
+ source: fetchResult.source
761
+ }, null, 2),
762
+ metadata: {
763
+ status: fetchResult.status,
764
+ artifactKind,
765
+ directoryPath,
766
+ source: fetchResult.source
767
+ }
768
+ };
769
+ }
770
+ const selection = selectWizardArtifacts(fetchResult.payload.artifacts, requestedArtifacts);
771
+ if (requestedArtifacts.length === 0) {
772
+ const authState = await resolveStoredAuthState(input.worktree, config);
773
+ const catalog = toWizardArtifactCatalog(fetchResult.payload, {
800
774
  pluginId: PLUGIN_ID,
801
- status: 'forbidden',
802
- message: 'This tool requires ADMIN or EDITOR role. Your current session does not have the required permission.'
775
+ availableTools: resolveAvailableTools(authState?.role ?? null)
776
+ });
777
+ return {
778
+ output: JSON.stringify({
779
+ ...catalog,
780
+ status: 'ready',
781
+ requestedDirectoryPath: directoryPath,
782
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
783
+ fetchedAt: fetchResult.fetchedAt,
784
+ source: fetchResult.source,
785
+ message: 'Provide artifact or artifacts to fetch DESIGN.md body/files.'
786
+ }, null, 2),
787
+ metadata: {
788
+ status: 'ready',
789
+ artifactKind,
790
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
791
+ }
792
+ };
793
+ }
794
+ if (selection.selectedItems.length === 0 && requestedArtifacts.length === 1) {
795
+ return {
796
+ output: JSON.stringify({
797
+ pluginId: PLUGIN_ID,
798
+ runtimeMode: 'tool_fetch_only',
799
+ status: 'not_found',
800
+ artifactKind,
801
+ requestedArtifact: requestedArtifacts[0],
802
+ availableArtifacts: fetchResult.payload.artifacts.map(toWizardArtifactSummary),
803
+ requestedDirectoryPath: directoryPath,
804
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution)
805
+ }, null, 2),
806
+ metadata: {
807
+ status: 'not_found',
808
+ artifactKind,
809
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
810
+ }
811
+ };
812
+ }
813
+ const detailResults = await Promise.all(selection.selectedItems.map(item => fetchWizardArtifactDetail({
814
+ worktree: input.worktree,
815
+ config,
816
+ resolution: workspaceResolution,
817
+ artifactKind,
818
+ artifactVersionId: item.artifactVersion.id,
819
+ signal: context.abort,
820
+ onAuthStateChanged: clearPublishedSkillState,
821
+ purpose: 'TOOL_FETCH'
822
+ })));
823
+ const failedDetail = detailResults.find(result => !result.ok);
824
+ if (failedDetail && !failedDetail.ok) {
825
+ return {
826
+ output: JSON.stringify({
827
+ pluginId: PLUGIN_ID,
828
+ runtimeMode: 'tool_fetch_only',
829
+ status: failedDetail.result.status,
830
+ artifactKind,
831
+ requestedDirectoryPath: directoryPath,
832
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
833
+ message: failedDetail.result.message,
834
+ fetchedAt: failedDetail.result.fetchedAt,
835
+ source: failedDetail.result.source
836
+ }, null, 2),
837
+ metadata: {
838
+ status: failedDetail.result.status,
839
+ artifactKind,
840
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
841
+ }
842
+ };
843
+ }
844
+ const details = detailResults.map((result, index) => {
845
+ if (!result.ok) throw new Error('Wizard artifact detail result unexpectedly missing after success guard.');
846
+ return toWizardArtifactDetail({
847
+ ...selection.selectedItems[index],
848
+ artifactVersion: result.artifact
849
+ });
850
+ });
851
+ return {
852
+ output: JSON.stringify({
853
+ pluginId: PLUGIN_ID,
854
+ runtimeMode: 'tool_fetch_only',
855
+ artifactKind,
856
+ requestedDirectoryPath: directoryPath,
857
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
858
+ workspace: fetchResult.payload.workspace,
859
+ fetchedAt: fetchResult.fetchedAt,
860
+ source: fetchResult.source,
861
+ requestedArtifacts,
862
+ missingArtifacts: selection.missingIdentifiers,
863
+ artifacts: details
803
864
  }, null, 2),
804
865
  metadata: {
805
- status: 'forbidden',
806
- role: authState?.role ?? 'none'
866
+ status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
867
+ artifactKind,
868
+ matchedCount: details.length.toString(),
869
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
807
870
  }
808
871
  };
809
872
  }
873
+ const result = await executePublishedSkillsFetchTool({
874
+ args: {
875
+ skill: args.artifact,
876
+ skills: args.artifacts,
877
+ directory: args.directory,
878
+ refresh: args.refresh
879
+ },
880
+ context
881
+ });
882
+ return withWizardArtifactEnvelope(result, artifactKind);
883
+ };
884
+ const executeStatusTool = async ({
885
+ args,
886
+ context
887
+ }) => {
810
888
  const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
811
889
  let snapshot = await resolvePluginStatusSnapshot({
812
890
  worktree: input.worktree,
@@ -844,20 +922,6 @@ export const OpencodeWizardSkillsPlugin = async input => {
844
922
  args,
845
923
  context
846
924
  }) => {
847
- const authState = await resolveStoredAuthState(input.worktree, config);
848
- if (!authState || authState.role !== 'ADMIN' && authState.role !== 'EDITOR') {
849
- return {
850
- output: JSON.stringify({
851
- pluginId: PLUGIN_ID,
852
- status: 'forbidden',
853
- message: 'This tool requires ADMIN or EDITOR role. Your current session does not have the required permission.'
854
- }, null, 2),
855
- metadata: {
856
- status: 'forbidden',
857
- role: authState?.role ?? 'none'
858
- }
859
- };
860
- }
861
925
  const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
862
926
  const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
863
927
  lastInteractiveDirectoryPath = directoryPath;
@@ -882,7 +946,13 @@ export const OpencodeWizardSkillsPlugin = async input => {
882
946
  signal: context.abort
883
947
  });
884
948
  if (!catalogResult.fetchResult.ok) {
885
- throw new Error(`Cannot resolve published skill preference target: ${catalogResult.fetchResult.message}`);
949
+ await emitPreferenceOutcome('PREFERENCE_FAILED');
950
+ return toFetchFailureOutput({
951
+ worktree: input.worktree,
952
+ config,
953
+ publishedSkillsResult: catalogResult,
954
+ loginBootstrapSnapshot: loginBootstrap.snapshot
955
+ });
886
956
  }
887
957
  const selectableCatalogSkills = catalogResult.fetchResult.payload.catalogSkills.map(item => ({
888
958
  ...item,
@@ -941,8 +1011,132 @@ export const OpencodeWizardSkillsPlugin = async input => {
941
1011
  };
942
1012
  } catch (error) {
943
1013
  await emitPreferenceOutcome('PREFERENCE_FAILED');
944
- throw error;
1014
+ const message = error instanceof Error ? error.message : 'Unknown published skill preference failure.';
1015
+ const metadata = {
1016
+ status: 'preference_failed',
1017
+ directoryPath
1018
+ };
1019
+ context.metadata({
1020
+ title: 'opencode-wizard published skill preference failed',
1021
+ metadata
1022
+ });
1023
+ return {
1024
+ output: JSON.stringify({
1025
+ pluginId: PLUGIN_ID,
1026
+ status: 'preference_failed',
1027
+ requestedIdentifier: requestedSkill,
1028
+ requestedDirectoryPath: directoryPath,
1029
+ message,
1030
+ guidance: 'The preference tool remains available. Check opencode_wizard_status for safe auth/catalog state, rerun fetch/status to bootstrap auth, and retry after catalog status is ready.'
1031
+ }, null, 2),
1032
+ metadata
1033
+ };
1034
+ }
1035
+ };
1036
+ const executeWizardArtifactPreferenceTool = async ({
1037
+ args,
1038
+ context
1039
+ }) => {
1040
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
1041
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
1042
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
1043
+ if (!artifactKind) {
1044
+ return buildUnsupportedWizardArtifactOutput({
1045
+ artifactKind: args.artifactKind ?? '',
1046
+ directoryPath
1047
+ });
1048
+ }
1049
+ if (artifactKind === 'DESIGN_DOC') {
1050
+ try {
1051
+ const action = toPublishedSkillPreferenceAction(args.action);
1052
+ const workspaceResolution = await resolveWorkspace({
1053
+ config,
1054
+ directory: requestedDirectory
1055
+ });
1056
+ const response = await fetchPublishedSkillsGraphQl({
1057
+ worktree: input.worktree,
1058
+ config,
1059
+ query: SET_WIZARD_ARTIFACT_PREFERENCE_MUTATION,
1060
+ variables: {
1061
+ input: {
1062
+ workspaceSlug: workspaceResolution.workspaceSlug,
1063
+ repositoryUrl: workspaceResolution.repositoryUrl,
1064
+ directoryPath: workspaceResolution.directoryPath,
1065
+ artifactKind,
1066
+ artifactSlug: args.artifact.trim(),
1067
+ preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project') === 'global' ? 'GLOBAL' : 'WORKSPACE',
1068
+ installed: action === 'install' ? true : action === 'uninstall' ? false : undefined,
1069
+ ignored: action === 'ignore' ? true : action === 'unignore' ? false : undefined
1070
+ }
1071
+ },
1072
+ signal: context.abort,
1073
+ onAuthStateChanged: clearPublishedSkillState
1074
+ });
1075
+ if (!response.ok) {
1076
+ return {
1077
+ output: JSON.stringify({
1078
+ pluginId: PLUGIN_ID,
1079
+ status: response.result.status,
1080
+ artifactKind,
1081
+ requestedIdentifier: args.artifact,
1082
+ requestedDirectoryPath: directoryPath,
1083
+ message: response.result.message,
1084
+ fetchedAt: response.result.fetchedAt,
1085
+ source: response.result.source
1086
+ }, null, 2),
1087
+ metadata: {
1088
+ status: response.result.status,
1089
+ artifactKind,
1090
+ directoryPath
1091
+ }
1092
+ };
1093
+ }
1094
+ return {
1095
+ output: JSON.stringify({
1096
+ pluginId: PLUGIN_ID,
1097
+ status: 'updated',
1098
+ artifactKind,
1099
+ requestedIdentifier: args.artifact,
1100
+ action,
1101
+ requestedDirectoryPath: directoryPath,
1102
+ preferenceState: response.data.setWizardArtifactPreference,
1103
+ message: 'Wizard artifact preference updated through the generic backend API.'
1104
+ }, null, 2),
1105
+ metadata: {
1106
+ status: 'updated',
1107
+ artifactKind,
1108
+ directoryPath,
1109
+ action
1110
+ }
1111
+ };
1112
+ } catch (error) {
1113
+ return {
1114
+ output: JSON.stringify({
1115
+ pluginId: PLUGIN_ID,
1116
+ status: 'preference_failed',
1117
+ artifactKind,
1118
+ requestedIdentifier: args.artifact,
1119
+ requestedDirectoryPath: directoryPath,
1120
+ message: error instanceof Error ? error.message : 'Unknown wizard artifact preference failure.'
1121
+ }, null, 2),
1122
+ metadata: {
1123
+ status: 'preference_failed',
1124
+ artifactKind,
1125
+ directoryPath
1126
+ }
1127
+ };
1128
+ }
945
1129
  }
1130
+ const result = await executePublishedSkillPreferenceTool({
1131
+ args: {
1132
+ skill: args.artifact,
1133
+ action: args.action,
1134
+ preferenceScope: args.preferenceScope,
1135
+ directory: args.directory
1136
+ },
1137
+ context
1138
+ });
1139
+ return withWizardArtifactEnvelope(result, artifactKind);
946
1140
  };
947
1141
  const executeEditorPublishSkillTool = async ({
948
1142
  args,
@@ -1039,7 +1233,6 @@ export const OpencodeWizardSkillsPlugin = async input => {
1039
1233
  };
1040
1234
  };
1041
1235
  const role = initialAuthState?.role ?? null;
1042
- const hasSharedToolAccess = role === 'ADMIN' || role === 'EDITOR';
1043
1236
  const isEditor = role === 'EDITOR';
1044
1237
  const {
1045
1238
  sharedTools,
@@ -1049,10 +1242,22 @@ export const OpencodeWizardSkillsPlugin = async input => {
1049
1242
  args,
1050
1243
  context
1051
1244
  }),
1245
+ fetchWizardArtifactCatalog: (args, context) => executeWizardArtifactCatalogTool({
1246
+ args,
1247
+ context
1248
+ }),
1249
+ fetchWizardArtifacts: (args, context) => executeWizardArtifactFetchTool({
1250
+ args,
1251
+ context
1252
+ }),
1052
1253
  updatePublishedSkillPreference: (args, context) => executePublishedSkillPreferenceTool({
1053
1254
  args,
1054
1255
  context
1055
1256
  }),
1257
+ updateWizardArtifactPreference: (args, context) => executeWizardArtifactPreferenceTool({
1258
+ args,
1259
+ context
1260
+ }),
1056
1261
  getStatus: (args, context) => executeStatusTool({
1057
1262
  args,
1058
1263
  context
@@ -1063,7 +1268,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
1063
1268
  })
1064
1269
  });
1065
1270
  return {
1066
- tool: !hasSharedToolAccess ? {} : {
1271
+ tool: {
1067
1272
  ...sharedTools,
1068
1273
  ...(isEditor ? editorOnlyTools : {})
1069
1274
  },