@aexol/opencode-wizard 0.3.4 → 0.3.6

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 (71) hide show
  1. package/README.md +11 -7
  2. package/dist/graphql-operations.d.ts +6 -2
  3. package/dist/graphql-operations.js +173 -156
  4. package/dist/graphql-operations.js.map +1 -1
  5. package/dist/plugin-tools.d.ts +37 -0
  6. package/dist/plugin-tools.js +58 -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/import-sources.d.ts +27 -0
  23. package/dist/server/import-sources.js +115 -0
  24. package/dist/server/import-sources.js.map +1 -0
  25. package/dist/server/preferences.d.ts +22 -0
  26. package/dist/server/preferences.js +121 -0
  27. package/dist/server/preferences.js.map +1 -0
  28. package/dist/server/runtime.d.ts +3 -22
  29. package/dist/server/runtime.js +567 -242
  30. package/dist/server/runtime.js.map +1 -1
  31. package/dist/server/types.d.ts +87 -0
  32. package/dist/server/types.js.map +1 -1
  33. package/dist/server.d.ts +1 -0
  34. package/dist/server.js +1 -0
  35. package/dist/server.js.map +1 -1
  36. package/dist/smoke-published-skills.js +4 -4
  37. package/dist/smoke-published-skills.js.map +1 -1
  38. package/dist/tui/components/common.d.ts +0 -1
  39. package/dist/tui/components/common.js +6 -29
  40. package/dist/tui/components/common.js.map +1 -1
  41. package/dist/tui/components/status-content.js +1 -1
  42. package/dist/tui/components/status-content.js.map +1 -1
  43. package/dist/tui/constants.d.ts +0 -5
  44. package/dist/tui/constants.js +0 -5
  45. package/dist/tui/constants.js.map +1 -1
  46. package/dist/tui/formatting.d.ts +0 -3
  47. package/dist/tui/formatting.js +1 -13
  48. package/dist/tui/formatting.js.map +1 -1
  49. package/dist/tui/skill-helpers.d.ts +1 -11
  50. package/dist/tui/skill-helpers.js +0 -41
  51. package/dist/tui/skill-helpers.js.map +1 -1
  52. package/dist/tui/slots.d.ts +1 -1
  53. package/dist/tui/slots.js +1 -24
  54. package/dist/tui/slots.js.map +1 -1
  55. package/dist/tui/status.js +2 -2
  56. package/dist/tui/status.js.map +1 -1
  57. package/dist/tui/types.d.ts +0 -19
  58. package/dist/tui/types.js.map +1 -1
  59. package/package.json +1 -1
  60. package/dist/tui/components/preference-action-notice-row.d.ts +0 -5
  61. package/dist/tui/components/preference-action-notice-row.js +0 -17
  62. package/dist/tui/components/preference-action-notice-row.js.map +0 -1
  63. package/dist/tui/components/skill-catalog-row.d.ts +0 -8
  64. package/dist/tui/components/skill-catalog-row.js +0 -124
  65. package/dist/tui/components/skill-catalog-row.js.map +0 -1
  66. package/dist/tui/components/wizard-skills-dialog-content.d.ts +0 -9
  67. package/dist/tui/components/wizard-skills-dialog-content.js +0 -219
  68. package/dist/tui/components/wizard-skills-dialog-content.js.map +0 -1
  69. package/dist/tui/components/wizard-skills-dialog.d.ts +0 -7
  70. package/dist/tui/components/wizard-skills-dialog.js +0 -156
  71. package/dist/tui/components/wizard-skills-dialog.js.map +0 -1
@@ -1,226 +1,98 @@
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, IMPORT_WIZARD_ARTIFACT_SNAPSHOT_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 { fetchWizardArtifactImportSource, planWizardArtifactImport } from './import-sources.js';
19
+ import { getCatalogCacheKey, resolvePublishedSkillPreferenceCacheContext, setPublishedSkillIgnored, setPublishedSkillInstalled, toPublishedSkillPreferenceAction, toPublishedSkillPreferenceScope } from './preferences.js';
17
20
  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
21
  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.');
22
+ export { resolvePluginStatusSnapshotWithAuthBootstrap } from './auth-bootstrap.js';
23
+ export { setPublishedSkillIgnored, setPublishedSkillInstalled } from './preferences.js';
24
+ const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
25
+ return JSON.stringify([catalogCacheKey, skillVersionId]);
54
26
  };
55
- const resolvePublishedSkillPreferenceCacheContext = async config => {
56
- const authState = await readGlobalAuthState(config.authStatePath);
57
- return {
58
- userKey: toStoredUserKey(authState),
59
- preferenceVersion: publishedSkillPreferenceCacheVersion
60
- };
27
+ const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
28
+ return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
61
29
  };
62
- const getCatalogCacheKey = (workspaceResolution, preferenceContext) => {
63
- return JSON.stringify([workspaceResolution.cacheKey, preferenceContext.userKey, preferenceContext.preferenceVersion]);
30
+ const SUPPORTED_WIZARD_ARTIFACT_KINDS = ['SKILL', 'DESIGN_DOC'];
31
+ const DESIGN_DOC_UNSUPPORTED_MESSAGE = 'DESIGN_DOC is supported by generic wizard artifact persistence; use catalog/detail tools for assigned or installed documents.';
32
+ const toWizardArtifactKind = value => {
33
+ if (!value) return 'SKILL';
34
+ const normalized = value.trim().toUpperCase().replace(/[-\s]+/gu, '_');
35
+ if (normalized === 'SKILL') return 'SKILL';
36
+ if (normalized === 'DESIGN_DOC') return 'DESIGN_DOC';
37
+ return null;
64
38
  };
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;
39
+ const buildUnsupportedWizardArtifactOutput = ({
40
+ artifactKind,
41
+ directoryPath
42
+ }) => ({
43
+ output: JSON.stringify({
44
+ pluginId: PLUGIN_ID,
45
+ runtimeMode: 'tool_fetch_only',
46
+ status: artifactKind === 'DESIGN_DOC' ? 'unsupported' : 'bad_artifact_kind',
47
+ artifactKind,
48
+ supportedArtifactKinds: SUPPORTED_WIZARD_ARTIFACT_KINDS,
49
+ requestedDirectoryPath: directoryPath,
50
+ message: artifactKind === 'DESIGN_DOC' ? DESIGN_DOC_UNSUPPORTED_MESSAGE : 'Unsupported wizard artifact kind. Use SKILL or DESIGN_DOC.'
51
+ }, null, 2),
52
+ metadata: {
53
+ status: artifactKind === 'DESIGN_DOC' ? 'unsupported' : 'bad_artifact_kind',
54
+ artifactKind,
55
+ directoryPath
69
56
  }
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.');
57
+ });
58
+ const withWizardArtifactEnvelope = (result, artifactKind) => {
59
+ if (!result || typeof result !== 'object' || !('output' in result)) return result;
60
+ const outputResult = result;
61
+ if (typeof outputResult.output !== 'string') return result;
62
+ try {
63
+ return {
64
+ ...outputResult,
65
+ output: JSON.stringify({
66
+ ...JSON.parse(outputResult.output),
67
+ artifactKind,
68
+ supportedArtifactKinds: SUPPORTED_WIZARD_ARTIFACT_KINDS,
69
+ compatibilityAliases: ['opencode_wizard_published_skills_fetch', 'opencode_wizard_published_skill_preference_set']
70
+ }, null, 2),
71
+ metadata: {
72
+ ...(outputResult.metadata ?? {}),
73
+ artifactKind
87
74
  }
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);
75
+ };
76
+ } catch {
77
+ return result;
128
78
  }
129
- return withStatusMessage(snapshot, 'Browser login is pending from the TUI/status path.');
130
79
  };
131
- const toBackendPreferenceScope = preferenceScope => {
132
- if (preferenceScope === 'global') return 'GLOBAL';
133
- return 'WORKSPACE';
80
+ const matchesWizardArtifactIdentifier = (item, identifier) => {
81
+ const normalizedIdentifier = identifier.trim().toLowerCase();
82
+ if (!normalizedIdentifier) return false;
83
+ if (item.artifact.slug.toLowerCase() === normalizedIdentifier) return true;
84
+ if (item.artifact.name.toLowerCase() === normalizedIdentifier) return true;
85
+ if (item.artifactVersion.frontmatterName.toLowerCase() === normalizedIdentifier) return true;
86
+ return item.artifactVersion.id.toLowerCase() === normalizedIdentifier;
134
87
  };
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;
88
+ const selectWizardArtifacts = (items, identifiers) => {
89
+ const selectedItems = items.filter(item => identifiers.some(identifier => matchesWizardArtifactIdentifier(item, identifier)));
90
+ const missingIdentifiers = identifiers.filter(identifier => !selectedItems.some(item => matchesWizardArtifactIdentifier(item, identifier)));
168
91
  return {
169
- scopeKey: preferences.scopeKey,
170
- userKey: preferences.userKey,
171
- ignoredSkillSlugs: preferences.ignoredSkills.map(item => item.skill.slug),
172
- installedGlobalSkillSlugs: [],
173
- installedWorkspaceSkillSlugs: []
92
+ selectedItems,
93
+ missingIdentifiers
174
94
  };
175
95
  };
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
96
  export const OpencodeWizardSkillsPlugin = async input => {
225
97
  const {
226
98
  tool
@@ -561,20 +433,6 @@ export const OpencodeWizardSkillsPlugin = async input => {
561
433
  args,
562
434
  context
563
435
  }) => {
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
436
  const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
579
437
  const requestedSkills = parseRequestedSkillArgs(args);
580
438
  const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
@@ -632,7 +490,11 @@ export const OpencodeWizardSkillsPlugin = async input => {
632
490
  const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
633
491
  const isSingleRequest = requestedSkills.length === 1;
634
492
  if (requestedSkills.length === 0) {
635
- const catalog = toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload);
493
+ const authState = await resolveStoredAuthState(input.worktree, config);
494
+ const catalog = {
495
+ ...toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload),
496
+ availableTools: resolveAvailableTools(authState?.role ?? null)
497
+ };
636
498
  context.metadata({
637
499
  title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount} active`,
638
500
  metadata: {
@@ -789,24 +651,241 @@ export const OpencodeWizardSkillsPlugin = async input => {
789
651
  }
790
652
  };
791
653
  };
792
- const executeStatusTool = async ({
654
+ const executeWizardArtifactCatalogTool = async ({
793
655
  args,
794
656
  context
795
657
  }) => {
796
- const authState = await resolveStoredAuthState(input.worktree, config);
797
- if (!authState || authState.role !== 'ADMIN' && authState.role !== 'EDITOR') {
658
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
659
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
660
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
661
+ if (!artifactKind) {
662
+ return buildUnsupportedWizardArtifactOutput({
663
+ artifactKind: args.artifactKind ?? '',
664
+ directoryPath
665
+ });
666
+ }
667
+ if (artifactKind === 'DESIGN_DOC') {
668
+ const workspaceResolution = await resolveWorkspace({
669
+ config,
670
+ directory: requestedDirectory
671
+ });
672
+ const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, clearPublishedSkillState);
673
+ if (!fetchResult.ok) {
674
+ return {
675
+ output: JSON.stringify({
676
+ pluginId: PLUGIN_ID,
677
+ runtimeMode: 'tool_fetch_only',
678
+ status: fetchResult.status,
679
+ artifactKind,
680
+ requestedDirectoryPath: directoryPath,
681
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
682
+ message: fetchResult.message,
683
+ fetchedAt: fetchResult.fetchedAt,
684
+ source: fetchResult.source
685
+ }, null, 2),
686
+ metadata: {
687
+ status: fetchResult.status,
688
+ artifactKind,
689
+ directoryPath,
690
+ source: fetchResult.source
691
+ }
692
+ };
693
+ }
694
+ const authState = await resolveStoredAuthState(input.worktree, config);
695
+ const catalog = toWizardArtifactCatalog(fetchResult.payload, {
696
+ pluginId: PLUGIN_ID,
697
+ availableTools: resolveAvailableTools(authState?.role ?? null)
698
+ });
798
699
  return {
799
700
  output: JSON.stringify({
701
+ ...catalog,
702
+ status: 'ready',
703
+ requestedDirectoryPath: directoryPath,
704
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
705
+ fetchedAt: fetchResult.fetchedAt,
706
+ source: fetchResult.source,
707
+ message: 'DESIGN_DOC catalog discovery only. Full DESIGN.md bodies/files require opencode_wizard_artifact_fetch with artifactKind DESIGN_DOC.'
708
+ }, null, 2),
709
+ metadata: {
710
+ status: 'ready',
711
+ artifactKind,
712
+ artifactCount: catalog.artifactCount.toString(),
713
+ ...toWorkspaceResolutionMetadata(workspaceResolution),
714
+ source: fetchResult.source
715
+ }
716
+ };
717
+ }
718
+ const result = await executePublishedSkillsFetchTool({
719
+ args: {
720
+ directory: args.directory,
721
+ refresh: args.refresh
722
+ },
723
+ context
724
+ });
725
+ return withWizardArtifactEnvelope(result, artifactKind);
726
+ };
727
+ const executeWizardArtifactFetchTool = async ({
728
+ args,
729
+ context
730
+ }) => {
731
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
732
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
733
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
734
+ if (!artifactKind) {
735
+ return buildUnsupportedWizardArtifactOutput({
736
+ artifactKind: args.artifactKind ?? '',
737
+ directoryPath
738
+ });
739
+ }
740
+ if (artifactKind === 'DESIGN_DOC') {
741
+ const requestedArtifacts = parseRequestedSkillArgs({
742
+ skill: args.artifact,
743
+ skills: args.artifacts
744
+ });
745
+ const workspaceResolution = await resolveWorkspace({
746
+ config,
747
+ directory: requestedDirectory
748
+ });
749
+ const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, clearPublishedSkillState);
750
+ if (!fetchResult.ok) {
751
+ return {
752
+ output: JSON.stringify({
753
+ pluginId: PLUGIN_ID,
754
+ runtimeMode: 'tool_fetch_only',
755
+ status: fetchResult.status,
756
+ artifactKind,
757
+ requestedDirectoryPath: directoryPath,
758
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
759
+ message: fetchResult.message,
760
+ fetchedAt: fetchResult.fetchedAt,
761
+ source: fetchResult.source
762
+ }, null, 2),
763
+ metadata: {
764
+ status: fetchResult.status,
765
+ artifactKind,
766
+ directoryPath,
767
+ source: fetchResult.source
768
+ }
769
+ };
770
+ }
771
+ const selection = selectWizardArtifacts(fetchResult.payload.artifacts, requestedArtifacts);
772
+ if (requestedArtifacts.length === 0) {
773
+ const authState = await resolveStoredAuthState(input.worktree, config);
774
+ const catalog = toWizardArtifactCatalog(fetchResult.payload, {
800
775
  pluginId: PLUGIN_ID,
801
- status: 'forbidden',
802
- message: 'This tool requires ADMIN or EDITOR role. Your current session does not have the required permission.'
776
+ availableTools: resolveAvailableTools(authState?.role ?? null)
777
+ });
778
+ return {
779
+ output: JSON.stringify({
780
+ ...catalog,
781
+ status: 'ready',
782
+ requestedDirectoryPath: directoryPath,
783
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
784
+ fetchedAt: fetchResult.fetchedAt,
785
+ source: fetchResult.source,
786
+ message: 'Provide artifact or artifacts to fetch DESIGN.md body/files.'
787
+ }, null, 2),
788
+ metadata: {
789
+ status: 'ready',
790
+ artifactKind,
791
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
792
+ }
793
+ };
794
+ }
795
+ if (selection.selectedItems.length === 0 && requestedArtifacts.length === 1) {
796
+ return {
797
+ output: JSON.stringify({
798
+ pluginId: PLUGIN_ID,
799
+ runtimeMode: 'tool_fetch_only',
800
+ status: 'not_found',
801
+ artifactKind,
802
+ requestedArtifact: requestedArtifacts[0],
803
+ availableArtifacts: fetchResult.payload.artifacts.map(toWizardArtifactSummary),
804
+ requestedDirectoryPath: directoryPath,
805
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution)
806
+ }, null, 2),
807
+ metadata: {
808
+ status: 'not_found',
809
+ artifactKind,
810
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
811
+ }
812
+ };
813
+ }
814
+ const detailResults = await Promise.all(selection.selectedItems.map(item => fetchWizardArtifactDetail({
815
+ worktree: input.worktree,
816
+ config,
817
+ resolution: workspaceResolution,
818
+ artifactKind,
819
+ artifactVersionId: item.artifactVersion.id,
820
+ signal: context.abort,
821
+ onAuthStateChanged: clearPublishedSkillState,
822
+ purpose: 'TOOL_FETCH'
823
+ })));
824
+ const failedDetail = detailResults.find(result => !result.ok);
825
+ if (failedDetail && !failedDetail.ok) {
826
+ return {
827
+ output: JSON.stringify({
828
+ pluginId: PLUGIN_ID,
829
+ runtimeMode: 'tool_fetch_only',
830
+ status: failedDetail.result.status,
831
+ artifactKind,
832
+ requestedDirectoryPath: directoryPath,
833
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
834
+ message: failedDetail.result.message,
835
+ fetchedAt: failedDetail.result.fetchedAt,
836
+ source: failedDetail.result.source
837
+ }, null, 2),
838
+ metadata: {
839
+ status: failedDetail.result.status,
840
+ artifactKind,
841
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
842
+ }
843
+ };
844
+ }
845
+ const details = detailResults.map((result, index) => {
846
+ if (!result.ok) throw new Error('Wizard artifact detail result unexpectedly missing after success guard.');
847
+ return toWizardArtifactDetail({
848
+ ...selection.selectedItems[index],
849
+ artifactVersion: result.artifact
850
+ });
851
+ });
852
+ return {
853
+ output: JSON.stringify({
854
+ pluginId: PLUGIN_ID,
855
+ runtimeMode: 'tool_fetch_only',
856
+ artifactKind,
857
+ requestedDirectoryPath: directoryPath,
858
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
859
+ workspace: fetchResult.payload.workspace,
860
+ fetchedAt: fetchResult.fetchedAt,
861
+ source: fetchResult.source,
862
+ requestedArtifacts,
863
+ missingArtifacts: selection.missingIdentifiers,
864
+ artifacts: details
803
865
  }, null, 2),
804
866
  metadata: {
805
- status: 'forbidden',
806
- role: authState?.role ?? 'none'
867
+ status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
868
+ artifactKind,
869
+ matchedCount: details.length.toString(),
870
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
807
871
  }
808
872
  };
809
873
  }
874
+ const result = await executePublishedSkillsFetchTool({
875
+ args: {
876
+ skill: args.artifact,
877
+ skills: args.artifacts,
878
+ directory: args.directory,
879
+ refresh: args.refresh
880
+ },
881
+ context
882
+ });
883
+ return withWizardArtifactEnvelope(result, artifactKind);
884
+ };
885
+ const executeStatusTool = async ({
886
+ args,
887
+ context
888
+ }) => {
810
889
  const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
811
890
  let snapshot = await resolvePluginStatusSnapshot({
812
891
  worktree: input.worktree,
@@ -844,20 +923,6 @@ export const OpencodeWizardSkillsPlugin = async input => {
844
923
  args,
845
924
  context
846
925
  }) => {
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
926
  const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
862
927
  const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
863
928
  lastInteractiveDirectoryPath = directoryPath;
@@ -882,7 +947,13 @@ export const OpencodeWizardSkillsPlugin = async input => {
882
947
  signal: context.abort
883
948
  });
884
949
  if (!catalogResult.fetchResult.ok) {
885
- throw new Error(`Cannot resolve published skill preference target: ${catalogResult.fetchResult.message}`);
950
+ await emitPreferenceOutcome('PREFERENCE_FAILED');
951
+ return toFetchFailureOutput({
952
+ worktree: input.worktree,
953
+ config,
954
+ publishedSkillsResult: catalogResult,
955
+ loginBootstrapSnapshot: loginBootstrap.snapshot
956
+ });
886
957
  }
887
958
  const selectableCatalogSkills = catalogResult.fetchResult.payload.catalogSkills.map(item => ({
888
959
  ...item,
@@ -941,8 +1012,132 @@ export const OpencodeWizardSkillsPlugin = async input => {
941
1012
  };
942
1013
  } catch (error) {
943
1014
  await emitPreferenceOutcome('PREFERENCE_FAILED');
944
- throw error;
1015
+ const message = error instanceof Error ? error.message : 'Unknown published skill preference failure.';
1016
+ const metadata = {
1017
+ status: 'preference_failed',
1018
+ directoryPath
1019
+ };
1020
+ context.metadata({
1021
+ title: 'opencode-wizard published skill preference failed',
1022
+ metadata
1023
+ });
1024
+ return {
1025
+ output: JSON.stringify({
1026
+ pluginId: PLUGIN_ID,
1027
+ status: 'preference_failed',
1028
+ requestedIdentifier: requestedSkill,
1029
+ requestedDirectoryPath: directoryPath,
1030
+ message,
1031
+ 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.'
1032
+ }, null, 2),
1033
+ metadata
1034
+ };
1035
+ }
1036
+ };
1037
+ const executeWizardArtifactPreferenceTool = async ({
1038
+ args,
1039
+ context
1040
+ }) => {
1041
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
1042
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
1043
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
1044
+ if (!artifactKind) {
1045
+ return buildUnsupportedWizardArtifactOutput({
1046
+ artifactKind: args.artifactKind ?? '',
1047
+ directoryPath
1048
+ });
1049
+ }
1050
+ if (artifactKind === 'DESIGN_DOC') {
1051
+ try {
1052
+ const action = toPublishedSkillPreferenceAction(args.action);
1053
+ const workspaceResolution = await resolveWorkspace({
1054
+ config,
1055
+ directory: requestedDirectory
1056
+ });
1057
+ const response = await fetchPublishedSkillsGraphQl({
1058
+ worktree: input.worktree,
1059
+ config,
1060
+ query: SET_WIZARD_ARTIFACT_PREFERENCE_MUTATION,
1061
+ variables: {
1062
+ input: {
1063
+ workspaceSlug: workspaceResolution.workspaceSlug,
1064
+ repositoryUrl: workspaceResolution.repositoryUrl,
1065
+ directoryPath: workspaceResolution.directoryPath,
1066
+ artifactKind,
1067
+ artifactSlug: args.artifact.trim(),
1068
+ preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project') === 'global' ? 'GLOBAL' : 'WORKSPACE',
1069
+ installed: action === 'install' ? true : action === 'uninstall' ? false : undefined,
1070
+ ignored: action === 'ignore' ? true : action === 'unignore' ? false : undefined
1071
+ }
1072
+ },
1073
+ signal: context.abort,
1074
+ onAuthStateChanged: clearPublishedSkillState
1075
+ });
1076
+ if (!response.ok) {
1077
+ return {
1078
+ output: JSON.stringify({
1079
+ pluginId: PLUGIN_ID,
1080
+ status: response.result.status,
1081
+ artifactKind,
1082
+ requestedIdentifier: args.artifact,
1083
+ requestedDirectoryPath: directoryPath,
1084
+ message: response.result.message,
1085
+ fetchedAt: response.result.fetchedAt,
1086
+ source: response.result.source
1087
+ }, null, 2),
1088
+ metadata: {
1089
+ status: response.result.status,
1090
+ artifactKind,
1091
+ directoryPath
1092
+ }
1093
+ };
1094
+ }
1095
+ return {
1096
+ output: JSON.stringify({
1097
+ pluginId: PLUGIN_ID,
1098
+ status: 'updated',
1099
+ artifactKind,
1100
+ requestedIdentifier: args.artifact,
1101
+ action,
1102
+ requestedDirectoryPath: directoryPath,
1103
+ preferenceState: response.data.setWizardArtifactPreference,
1104
+ message: 'Wizard artifact preference updated through the generic backend API.'
1105
+ }, null, 2),
1106
+ metadata: {
1107
+ status: 'updated',
1108
+ artifactKind,
1109
+ directoryPath,
1110
+ action
1111
+ }
1112
+ };
1113
+ } catch (error) {
1114
+ return {
1115
+ output: JSON.stringify({
1116
+ pluginId: PLUGIN_ID,
1117
+ status: 'preference_failed',
1118
+ artifactKind,
1119
+ requestedIdentifier: args.artifact,
1120
+ requestedDirectoryPath: directoryPath,
1121
+ message: error instanceof Error ? error.message : 'Unknown wizard artifact preference failure.'
1122
+ }, null, 2),
1123
+ metadata: {
1124
+ status: 'preference_failed',
1125
+ artifactKind,
1126
+ directoryPath
1127
+ }
1128
+ };
1129
+ }
945
1130
  }
1131
+ const result = await executePublishedSkillPreferenceTool({
1132
+ args: {
1133
+ skill: args.artifact,
1134
+ action: args.action,
1135
+ preferenceScope: args.preferenceScope,
1136
+ directory: args.directory
1137
+ },
1138
+ context
1139
+ });
1140
+ return withWizardArtifactEnvelope(result, artifactKind);
946
1141
  };
947
1142
  const executeEditorPublishSkillTool = async ({
948
1143
  args,
@@ -1038,8 +1233,122 @@ export const OpencodeWizardSkillsPlugin = async input => {
1038
1233
  }
1039
1234
  };
1040
1235
  };
1236
+ const executeWizardArtifactImportTool = async ({
1237
+ args,
1238
+ context
1239
+ }) => {
1240
+ const authState = await resolveStoredAuthState(input.worktree, config);
1241
+ const importPlan = planWizardArtifactImport({
1242
+ artifactKind: args.artifactKind,
1243
+ source: args.source,
1244
+ slug: args.slug,
1245
+ skill: args.skill
1246
+ });
1247
+ if (!authState || authState.role !== 'EDITOR') {
1248
+ return {
1249
+ output: JSON.stringify({
1250
+ pluginId: PLUGIN_ID,
1251
+ status: 'forbidden',
1252
+ artifactKind: importPlan.artifactKind,
1253
+ source: importPlan.source,
1254
+ message: 'This tool requires EDITOR role. Your current session does not have the required editor permission.'
1255
+ }, null, 2),
1256
+ metadata: {
1257
+ status: 'forbidden',
1258
+ role: authState?.role ?? 'none'
1259
+ }
1260
+ };
1261
+ }
1262
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
1263
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
1264
+ lastInteractiveDirectoryPath = directoryPath;
1265
+ let importSource;
1266
+ try {
1267
+ importSource = await fetchWizardArtifactImportSource({
1268
+ artifactKind: args.artifactKind,
1269
+ source: args.source,
1270
+ slug: args.slug,
1271
+ skill: args.skill,
1272
+ signal: context.abort
1273
+ });
1274
+ } catch (error) {
1275
+ return {
1276
+ output: JSON.stringify({
1277
+ pluginId: PLUGIN_ID,
1278
+ status: 'source_fetch_failed',
1279
+ artifactKind: importPlan.artifactKind,
1280
+ source: importPlan.source,
1281
+ candidateUrls: importPlan.candidateUrls,
1282
+ message: error instanceof Error ? error.message : 'Unknown import source fetch failure.'
1283
+ }, null, 2),
1284
+ metadata: {
1285
+ status: 'source_fetch_failed',
1286
+ artifactKind: importPlan.artifactKind,
1287
+ directoryPath
1288
+ }
1289
+ };
1290
+ }
1291
+ const response = await fetchPublishedSkillsGraphQl({
1292
+ worktree: input.worktree,
1293
+ config,
1294
+ query: IMPORT_WIZARD_ARTIFACT_SNAPSHOT_MUTATION,
1295
+ variables: {
1296
+ input: {
1297
+ artifactKind: importSource.artifactKind,
1298
+ slug: importSource.slug,
1299
+ name: args.name,
1300
+ summary: args.summary,
1301
+ markdownContent: importSource.markdownContent,
1302
+ source: importSource.fetchedUrl,
1303
+ sourceSnapshotRef: importSource.sourceRef
1304
+ }
1305
+ },
1306
+ signal: context.abort
1307
+ });
1308
+ if (!response.ok) {
1309
+ return {
1310
+ output: JSON.stringify({
1311
+ pluginId: PLUGIN_ID,
1312
+ status: 'request_failed',
1313
+ artifactKind: importSource.artifactKind,
1314
+ source: importSource.source,
1315
+ fetchedUrl: importSource.fetchedUrl,
1316
+ message: response.result.message
1317
+ }, null, 2),
1318
+ metadata: {
1319
+ status: 'request_failed',
1320
+ artifactKind: importSource.artifactKind,
1321
+ directoryPath
1322
+ }
1323
+ };
1324
+ }
1325
+ const payload = response.data.importWizardArtifactSnapshot;
1326
+ await scheduleInteractivePresenceStart();
1327
+ return {
1328
+ output: JSON.stringify({
1329
+ pluginId: PLUGIN_ID,
1330
+ status: payload.success ? 'imported' : 'import_failed',
1331
+ artifactKind: importSource.artifactKind,
1332
+ artifactSlug: payload.artifactSlug,
1333
+ artifactVersionId: payload.artifactVersionId,
1334
+ skillSlug: payload.skillSlug,
1335
+ skillVersionId: payload.skillVersionId,
1336
+ source: importSource.source,
1337
+ fetchedUrl: importSource.fetchedUrl,
1338
+ sourceSnapshotRef: importSource.sourceRef,
1339
+ errors: payload.errors,
1340
+ message: 'Imported backend artifact is now the wizard/plugin runtime authority; no project files were written.'
1341
+ }, null, 2),
1342
+ metadata: {
1343
+ status: payload.success ? 'imported' : 'import_failed',
1344
+ artifactKind: importSource.artifactKind,
1345
+ artifactSlug: payload.artifactSlug ?? '',
1346
+ artifactVersionId: payload.artifactVersionId ?? '',
1347
+ directoryPath
1348
+ }
1349
+ };
1350
+ };
1041
1351
  const role = initialAuthState?.role ?? null;
1042
- const hasSharedToolAccess = role === 'ADMIN' || role === 'EDITOR';
1043
1352
  const isEditor = role === 'EDITOR';
1044
1353
  const {
1045
1354
  sharedTools,
@@ -1049,10 +1358,22 @@ export const OpencodeWizardSkillsPlugin = async input => {
1049
1358
  args,
1050
1359
  context
1051
1360
  }),
1361
+ fetchWizardArtifactCatalog: (args, context) => executeWizardArtifactCatalogTool({
1362
+ args,
1363
+ context
1364
+ }),
1365
+ fetchWizardArtifacts: (args, context) => executeWizardArtifactFetchTool({
1366
+ args,
1367
+ context
1368
+ }),
1052
1369
  updatePublishedSkillPreference: (args, context) => executePublishedSkillPreferenceTool({
1053
1370
  args,
1054
1371
  context
1055
1372
  }),
1373
+ updateWizardArtifactPreference: (args, context) => executeWizardArtifactPreferenceTool({
1374
+ args,
1375
+ context
1376
+ }),
1056
1377
  getStatus: (args, context) => executeStatusTool({
1057
1378
  args,
1058
1379
  context
@@ -1060,10 +1381,14 @@ export const OpencodeWizardSkillsPlugin = async input => {
1060
1381
  publishEditorSkill: (args, context) => executeEditorPublishSkillTool({
1061
1382
  args,
1062
1383
  context
1384
+ }),
1385
+ importWizardArtifact: (args, context) => executeWizardArtifactImportTool({
1386
+ args,
1387
+ context
1063
1388
  })
1064
1389
  });
1065
1390
  return {
1066
- tool: !hasSharedToolAccess ? {} : {
1391
+ tool: {
1067
1392
  ...sharedTools,
1068
1393
  ...(isEditor ? editorOnlyTools : {})
1069
1394
  },