@aexol/opencode-wizard 0.3.10 → 0.3.13

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 (40) hide show
  1. package/README.md +4 -1
  2. package/dist/graphql-operations.d.ts +5 -4
  3. package/dist/graphql-operations.js +27 -2
  4. package/dist/graphql-operations.js.map +1 -1
  5. package/dist/plugin-tools.d.ts +3 -0
  6. package/dist/plugin-tools.js +12 -9
  7. package/dist/plugin-tools.js.map +1 -1
  8. package/dist/published-skills-system-note.js +1 -1
  9. package/dist/published-skills-system-note.js.map +1 -1
  10. package/dist/published-skills-transform.d.ts +22 -4
  11. package/dist/published-skills-transform.js +21 -5
  12. package/dist/published-skills-transform.js.map +1 -1
  13. package/dist/server/client.d.ts +11 -4
  14. package/dist/server/client.js +89 -6
  15. package/dist/server/client.js.map +1 -1
  16. package/dist/server/preferences.js +2 -1
  17. package/dist/server/preferences.js.map +1 -1
  18. package/dist/server/runtime.d.ts +1 -1
  19. package/dist/server/runtime.js +178 -27
  20. package/dist/server/runtime.js.map +1 -1
  21. package/dist/server/status.js +6 -2
  22. package/dist/server/status.js.map +1 -1
  23. package/dist/server/types.d.ts +17 -4
  24. package/dist/server/types.js.map +1 -1
  25. package/dist/smoke-published-skills.js +6 -3
  26. package/dist/smoke-published-skills.js.map +1 -1
  27. package/dist/tui/components/skill-picker-dialog.d.ts +7 -0
  28. package/dist/tui/components/skill-picker-dialog.js +94 -0
  29. package/dist/tui/components/skill-picker-dialog.js.map +1 -0
  30. package/dist/tui/components/status-content.d.ts +4 -0
  31. package/dist/tui/components/status-content.js +145 -20
  32. package/dist/tui/components/status-content.js.map +1 -1
  33. package/dist/tui/plugin.js +36 -69
  34. package/dist/tui/plugin.js.map +1 -1
  35. package/dist/tui/skill-helpers.d.ts +15 -2
  36. package/dist/tui/skill-helpers.js +82 -20
  37. package/dist/tui/skill-helpers.js.map +1 -1
  38. package/dist/tui/slots.js +13 -3
  39. package/dist/tui/slots.js.map +1 -1
  40. package/package.json +2 -2
@@ -6,11 +6,11 @@ import { resolveStoredAuthState, toAuthState, writeAuthState } from './auth-stor
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, fetchWizardArtifactDetail, fetchWizardArtifactsCatalog, maybePersistWorkspaceSlugFromCatalog } from './client.js';
9
+ import { fetchPublishedSkillDetail, fetchPublishedSkillsCatalog, fetchPublishedSkillsGraphQl, fetchWizardArtifactDetail, fetchWizardArtifactsCatalog, hydrateStoredAuthStateRole, maybePersistWorkspaceSlugFromCatalog } from './client.js';
10
10
  import { normalizeAbsolutePath } from './path-utils.js';
11
11
  import { emitPluginActionEvent, emitPresenceEvent } from './presence.js';
12
12
  import { normalizeDirectoryArg, normalizeRepositoryPath, resolveWorkspace, toWorkspaceResolutionMetadata, toWorkspaceResolutionOutput } from './workspace.js';
13
- import { parseRequestedSkillArgs, selectPublishedSkills, toPublishedSkillDetail, toPublishedSkillSummary, toWizardArtifactCatalog, toWizardArtifactDetail, toWizardArtifactSummary } from '../published-skills-transform.js';
13
+ import { parseRequestedSkillArgs, selectPublishedSkills, toInstallableSkillSummary, toPublishedSkillDetail, toPublishedSkillSummary, toWizardArtifactCatalog, toWizardArtifactDetail, toWizardArtifactSummary } from '../published-skills-transform.js';
14
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
16
  import { buildSystemNote, filterIgnoredPublishedSkills, resolvePluginStatusSnapshot, toAiFacingPluginStatusSnapshot, toFetchFailureOutput, toPluginStatusMetadata, toPublishedSkillCatalog } from './status.js';
@@ -20,7 +20,7 @@ import { getCatalogCacheKey, resolvePublishedSkillPreferenceCacheContext, setPub
20
20
  export { buildSystemNote, resolvePluginStatusSnapshot, toPluginAuthStateSummary, toPublishedSkillCatalog } from './status.js';
21
21
  const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
22
22
  export { resolvePluginStatusSnapshotWithAuthBootstrap } from './auth-bootstrap.js';
23
- export { setPublishedSkillIgnored, setPublishedSkillInstalled } from './preferences.js';
23
+ export { setPublishedSkillIgnored, setPublishedSkillInstalled, toPublishedSkillPreferenceScope } from './preferences.js';
24
24
  const getDetailCacheKey = (catalogCacheKey, skillVersionId, revision) => {
25
25
  return JSON.stringify([catalogCacheKey, skillVersionId, revision]);
26
26
  };
@@ -118,6 +118,105 @@ const selectWizardArtifacts = (items, identifiers) => {
118
118
  missingIdentifiers
119
119
  };
120
120
  };
121
+ const MAX_IDENTIFIER_SUGGESTIONS = 3;
122
+ const RECOMMENDATION_METADATA_NOTE = 'Recommendation fields are backend metadata for routing and discoverability only; fetch/detail tools are still required before using artifact bodies as guidance.';
123
+ const normalizeRecommendationContext = args => {
124
+ if (typeof args.recommendationContext !== 'string') return null;
125
+ const recommendationContext = args.recommendationContext.trim();
126
+ if (!recommendationContext) return null;
127
+ return recommendationContext;
128
+ };
129
+ const normalizeSuggestionIdentifier = value => value.trim().toLowerCase().normalize('NFKD').replace(/[^\p{Letter}\p{Number}]+/gu, '');
130
+ const getEditDistance = (left, right) => {
131
+ if (left === right) return 0;
132
+ if (!left) return right.length;
133
+ if (!right) return left.length;
134
+ const previous = Array.from({
135
+ length: right.length + 1
136
+ }, (_, index) => index);
137
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
138
+ const current = [leftIndex];
139
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
140
+ const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
141
+ current[rightIndex] = Math.min(current[rightIndex - 1] + 1, previous[rightIndex] + 1, previous[rightIndex - 1] + substitutionCost);
142
+ }
143
+ previous.splice(0, previous.length, ...current);
144
+ }
145
+ return previous[right.length];
146
+ };
147
+ const getSuggestionScore = (requested, candidate) => {
148
+ const normalizedRequested = normalizeSuggestionIdentifier(requested);
149
+ const normalizedCandidate = normalizeSuggestionIdentifier(candidate);
150
+ if (!normalizedRequested || !normalizedCandidate) return null;
151
+ if (normalizedRequested === normalizedCandidate) return 0;
152
+ if (normalizedCandidate.includes(normalizedRequested) || normalizedRequested.includes(normalizedCandidate)) return 1;
153
+ const distance = getEditDistance(normalizedRequested, normalizedCandidate);
154
+ const threshold = Math.max(2, Math.ceil(Math.max(normalizedRequested.length, normalizedCandidate.length) * 0.35));
155
+ if (distance > threshold) return null;
156
+ return distance + 2;
157
+ };
158
+ const dedupeSuggestionCandidates = candidates => {
159
+ const seen = new Set();
160
+ return candidates.filter(candidate => {
161
+ const key = normalizeSuggestionIdentifier(candidate.identifier);
162
+ if (seen.has(key)) return false;
163
+ seen.add(key);
164
+ return true;
165
+ });
166
+ };
167
+ const getIdentifierSuggestions = (requestedIdentifier, candidates) => dedupeSuggestionCandidates(candidates).map(candidate => ({
168
+ candidate,
169
+ score: getSuggestionScore(requestedIdentifier, candidate.identifier)
170
+ })).filter(result => result.score !== null).sort((left, right) => {
171
+ if (left.score !== right.score) return left.score - right.score;
172
+ if (left.candidate.source !== right.candidate.source) return left.candidate.source === 'active' ? -1 : 1;
173
+ return left.candidate.identifier.localeCompare(right.candidate.identifier);
174
+ }).slice(0, MAX_IDENTIFIER_SUGGESTIONS).map(({
175
+ candidate
176
+ }) => ({
177
+ identifier: candidate.canonicalIdentifier,
178
+ matchedIdentifier: candidate.identifier,
179
+ label: candidate.label,
180
+ source: candidate.source
181
+ }));
182
+ const getSkillSuggestionCandidates = payload => [...payload.skills.flatMap(item => {
183
+ const summary = toPublishedSkillSummary(item);
184
+ return summary.identifiers.map(identifier => ({
185
+ identifier,
186
+ canonicalIdentifier: summary.skillSlug,
187
+ label: summary.artifactName || summary.skillName || summary.skillSlug,
188
+ source: 'active'
189
+ }));
190
+ }), ...payload.catalogSkills.flatMap(item => {
191
+ const summary = toInstallableSkillSummary(item);
192
+ return summary.identifiers.map(identifier => ({
193
+ identifier,
194
+ canonicalIdentifier: summary.skillSlug,
195
+ label: summary.artifactName || summary.skillName || summary.skillSlug,
196
+ source: 'catalog'
197
+ }));
198
+ })];
199
+ const getWizardArtifactSuggestionCandidates = (items, catalogItems) => [...items.flatMap(item => {
200
+ const summary = toWizardArtifactSummary(item);
201
+ return summary.identifiers.map(identifier => ({
202
+ identifier,
203
+ canonicalIdentifier: summary.artifactSlug,
204
+ label: summary.artifactName || summary.artifactSlug,
205
+ source: 'active'
206
+ }));
207
+ }), ...toWizardArtifactCatalogCursorItems(catalogItems).flatMap(item => {
208
+ const summary = toWizardArtifactSummary(item);
209
+ return summary.identifiers.map(identifier => ({
210
+ identifier,
211
+ canonicalIdentifier: summary.artifactSlug,
212
+ label: summary.artifactName || summary.artifactSlug,
213
+ source: 'catalog'
214
+ }));
215
+ })];
216
+ const getMissingIdentifierSuggestions = (identifiers, candidates) => identifiers.map(identifier => ({
217
+ identifier,
218
+ suggestions: getIdentifierSuggestions(identifier, candidates)
219
+ }));
121
220
  export const OpencodeWizardSkillsPlugin = async input => {
122
221
  const {
123
222
  tool
@@ -130,7 +229,12 @@ export const OpencodeWizardSkillsPlugin = async input => {
130
229
  const wizardArtifactDetailCache = new Map();
131
230
  const detailInflight = new Map();
132
231
  const wizardArtifactDetailInflight = new Map();
133
- const initialAuthState = await resolveStoredAuthState(input.worktree, config);
232
+ const initialAuthState = await hydrateStoredAuthStateRole({
233
+ worktree: input.worktree,
234
+ config,
235
+ signal: AbortSignal.timeout(5_000),
236
+ onAuthStateChanged: clearPublishedSkillState
237
+ });
134
238
  const registeredTools = resolveAvailableTools(initialAuthState?.role ?? null);
135
239
  const loginBootstrap = {
136
240
  promise: null,
@@ -221,14 +325,14 @@ export const OpencodeWizardSkillsPlugin = async input => {
221
325
  continue;
222
326
  }
223
327
  }
224
- const clearPublishedSkillState = () => {
328
+ function clearPublishedSkillState() {
225
329
  cache.clear();
226
330
  catalogInflight.clear();
227
331
  detailCache.clear();
228
332
  wizardArtifactDetailCache.clear();
229
333
  detailInflight.clear();
230
334
  wizardArtifactDetailInflight.clear();
231
- };
335
+ }
232
336
  const persistAuthState = async session => {
233
337
  const authState = toAuthState(session);
234
338
  await writeAuthState(config.authStatePath, authState);
@@ -334,7 +438,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
334
438
  const loadPublishedSkillCatalog = async ({
335
439
  directory,
336
440
  useCache,
337
- signal
441
+ signal,
442
+ recommendationContext
338
443
  }) => {
339
444
  const workspaceResolution = await resolveWorkspace({
340
445
  config,
@@ -342,7 +447,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
342
447
  });
343
448
  const directoryPath = workspaceResolution.directoryPath;
344
449
  const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
345
- const cacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
450
+ const cacheKey = JSON.stringify([getCatalogCacheKey(workspaceResolution, preferenceContext), recommendationContext ?? '']);
346
451
  const cached = cache.get(cacheKey);
347
452
  if (useCache && cached && cached.expiresAt > Date.now()) {
348
453
  return {
@@ -359,7 +464,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
359
464
  return inflight;
360
465
  }
361
466
  const requestPromise = (async () => {
362
- const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
467
+ const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, recommendationContext, clearPublishedSkillState);
363
468
  await maybePersistWorkspaceSlugFromCatalog({
364
469
  config,
365
470
  resolution: workspaceResolution,
@@ -416,6 +521,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
416
521
  config,
417
522
  resolution: workspaceResolution,
418
523
  skillVersionId: item.skillVersion.id,
524
+ artifactVersionId: item.publishedArtifact.id,
419
525
  signal,
420
526
  onAuthStateChanged: clearPublishedSkillState,
421
527
  purpose
@@ -524,6 +630,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
524
630
  }) => {
525
631
  const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
526
632
  const requestedSkills = parseRequestedSkillArgs(args);
633
+ const recommendationContext = normalizeRecommendationContext(args);
527
634
  const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
528
635
  lastInteractiveDirectoryPath = fetchActionDirectoryPath;
529
636
  const emitFetchOutcome = async event => {
@@ -535,7 +642,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
535
642
  let publishedSkillsResult = await loadPublishedSkillCatalog({
536
643
  directory: requestedDirectory,
537
644
  useCache: !args.refresh,
538
- signal: context.abort
645
+ signal: context.abort,
646
+ recommendationContext
539
647
  });
540
648
  if (publishedSkillsResult.fetchResult.ok) {
541
649
  await scheduleInteractivePresenceStart();
@@ -548,7 +656,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
548
656
  publishedSkillsResult = await loadPublishedSkillCatalog({
549
657
  directory: requestedDirectory,
550
658
  useCache: false,
551
- signal: context.abort
659
+ signal: context.abort,
660
+ recommendationContext
552
661
  });
553
662
  if (publishedSkillsResult.fetchResult.ok) {
554
663
  await scheduleInteractivePresenceStart();
@@ -578,6 +687,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
578
687
  }
579
688
  const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
580
689
  const isSingleRequest = requestedSkills.length === 1;
690
+ const skillSuggestionCandidates = getSkillSuggestionCandidates(filteredPublishedSkillsResult.fetchResult.payload);
581
691
  if (requestedSkills.length === 0) {
582
692
  const catalog = toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload, registeredTools);
583
693
  context.metadata({
@@ -605,9 +715,12 @@ export const OpencodeWizardSkillsPlugin = async input => {
605
715
  },
606
716
  fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
607
717
  source: filteredPublishedSkillsResult.fetchResult.source,
718
+ recommendationContext,
719
+ recommendationNote: RECOMMENDATION_METADATA_NOTE,
608
720
  cacheCursor: getCatalogCursor(filteredPublishedSkillsResult.fetchResult.payload.skills, filteredPublishedSkillsResult.fetchResult.payload.catalogSkills),
609
721
  cacheTtlMs: CACHE_TTL_MS,
610
- message: args.refresh ? 'Catalog discovery refreshed from the backend. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.' : 'Catalog discovery only. Cached results include deterministic revision cursors; pass `refresh: true` to force a backend refresh immediately. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.'
722
+ message: args.refresh ? 'Catalog discovery refreshed from the backend. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.' : 'Catalog discovery only. Cached results include deterministic revision cursors; pass `refresh: true` to force a backend refresh immediately. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.',
723
+ nextStep: 'Choose an active skill identifier from `skills` and call `opencode_wizard_published_skills_fetch` with `skill`; catalog-only skills must be installed before their bodies are fetchable.'
611
724
  }, null, 2),
612
725
  metadata: {
613
726
  status: 'ready',
@@ -622,7 +735,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
622
735
  }
623
736
  };
624
737
  }
625
- if (selection.selectedItems.length === 0 && isSingleRequest) {
738
+ if (selection.selectedItems.length === 0) {
626
739
  await emitFetchOutcome('FETCH_FAILED');
627
740
  return {
628
741
  output: JSON.stringify({
@@ -631,12 +744,19 @@ export const OpencodeWizardSkillsPlugin = async input => {
631
744
  status: 'not_found',
632
745
  requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
633
746
  workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
634
- requestedSkill: requestedSkills[0],
747
+ requestedSkill: isSingleRequest ? requestedSkills[0] : undefined,
748
+ requestedSkills,
749
+ missingSkills: selection.missingIdentifiers,
750
+ recommendationContext,
751
+ recommendationNote: RECOMMENDATION_METADATA_NOTE,
752
+ suggestions: isSingleRequest ? getIdentifierSuggestions(requestedSkills[0] ?? '', skillSuggestionCandidates) : undefined,
753
+ missingSkillSuggestions: getMissingIdentifierSuggestions(selection.missingIdentifiers, skillSuggestionCandidates),
635
754
  availableSkills: filteredPublishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary),
636
755
  ignoredPublishedSkills: {
637
756
  scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
638
757
  count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
639
- }
758
+ },
759
+ nextStep: 'Retry with one suggested identifier, fetch the catalog with no args/refresh to inspect available skills, or install a catalog-only suggestion before fetching its body.'
640
760
  }, null, 2),
641
761
  metadata: {
642
762
  status: 'not_found',
@@ -698,6 +818,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
698
818
  workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
699
819
  fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
700
820
  source: filteredPublishedSkillsResult.fetchResult.source,
821
+ recommendationContext,
822
+ recommendationNote: RECOMMENDATION_METADATA_NOTE,
701
823
  skill: detail
702
824
  }, null, 2),
703
825
  metadata: {
@@ -728,6 +850,10 @@ export const OpencodeWizardSkillsPlugin = async input => {
728
850
  source: filteredPublishedSkillsResult.fetchResult.source,
729
851
  requestedSkills,
730
852
  missingSkills: selection.missingIdentifiers,
853
+ recommendationContext,
854
+ recommendationNote: RECOMMENDATION_METADATA_NOTE,
855
+ missingSkillSuggestions: getMissingIdentifierSuggestions(selection.missingIdentifiers, skillSuggestionCandidates),
856
+ nextStep: selection.missingIdentifiers.length > 0 ? 'Fetched matched skills. Retry missing identifiers with suggested active names, or inspect the no-arg catalog for catalog-only skills that need installation first.' : 'Use each fetched `skill.markdownDocument` as reference content for matched backend-published skills.',
731
857
  skills: skillDetails
732
858
  }, null, 2),
733
859
  metadata: {
@@ -752,11 +878,12 @@ export const OpencodeWizardSkillsPlugin = async input => {
752
878
  });
753
879
  }
754
880
  if (artifactKind === 'DESIGN_DOC') {
881
+ const recommendationContext = normalizeRecommendationContext(args);
755
882
  const workspaceResolution = await resolveWorkspace({
756
883
  config,
757
884
  directory: requestedDirectory
758
885
  });
759
- const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, clearPublishedSkillState);
886
+ const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, recommendationContext, clearPublishedSkillState);
760
887
  if (!fetchResult.ok) {
761
888
  return {
762
889
  output: JSON.stringify({
@@ -791,9 +918,12 @@ export const OpencodeWizardSkillsPlugin = async input => {
791
918
  workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
792
919
  fetchedAt: fetchResult.fetchedAt,
793
920
  source: fetchResult.source,
921
+ recommendationContext,
922
+ recommendationNote: RECOMMENDATION_METADATA_NOTE,
794
923
  cacheCursor,
795
924
  cacheTtlMs: CACHE_TTL_MS,
796
- message: 'Generic artifact catalog discovery only. Full bodies/files require opencode_wizard_artifact_fetch with artifactKind and artifact identifiers.'
925
+ message: 'Generic artifact catalog discovery only. Full bodies/files require opencode_wizard_artifact_fetch with artifactKind and artifact identifiers.',
926
+ nextStep: 'Choose an active artifact identifier from `artifacts` and call `opencode_wizard_artifact_fetch`; catalog-only artifacts must be installed before their bodies are fetchable.'
797
927
  }, null, 2),
798
928
  metadata: {
799
929
  status: 'ready',
@@ -809,7 +939,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
809
939
  const result = await executePublishedSkillsFetchTool({
810
940
  args: {
811
941
  directory: args.directory,
812
- refresh: args.refresh
942
+ refresh: args.refresh,
943
+ recommendationContext: args.recommendationContext
813
944
  },
814
945
  context
815
946
  });
@@ -833,11 +964,12 @@ export const OpencodeWizardSkillsPlugin = async input => {
833
964
  skill: args.artifact,
834
965
  skills: args.artifacts
835
966
  });
967
+ const recommendationContext = normalizeRecommendationContext(args);
836
968
  const workspaceResolution = await resolveWorkspace({
837
969
  config,
838
970
  directory: requestedDirectory
839
971
  });
840
- const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, clearPublishedSkillState);
972
+ const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, recommendationContext, clearPublishedSkillState);
841
973
  if (!fetchResult.ok) {
842
974
  return {
843
975
  output: JSON.stringify({
@@ -860,6 +992,7 @@ export const OpencodeWizardSkillsPlugin = async input => {
860
992
  };
861
993
  }
862
994
  const selection = selectWizardArtifacts(fetchResult.payload.artifacts, requestedArtifacts);
995
+ const artifactSuggestionCandidates = getWizardArtifactSuggestionCandidates(fetchResult.payload.artifacts, fetchResult.payload.catalogArtifacts);
863
996
  if (requestedArtifacts.length === 0) {
864
997
  const catalog = toWizardArtifactCatalog(fetchResult.payload, {
865
998
  pluginId: PLUGIN_ID,
@@ -874,9 +1007,12 @@ export const OpencodeWizardSkillsPlugin = async input => {
874
1007
  workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
875
1008
  fetchedAt: fetchResult.fetchedAt,
876
1009
  source: fetchResult.source,
1010
+ recommendationContext,
1011
+ recommendationNote: RECOMMENDATION_METADATA_NOTE,
877
1012
  cacheCursor,
878
1013
  cacheTtlMs: CACHE_TTL_MS,
879
- message: 'Provide artifact or artifacts to fetch artifact body/files.'
1014
+ message: 'Provide artifact or artifacts to fetch artifact body/files.',
1015
+ nextStep: 'Choose an active artifact identifier from `artifacts` and call `opencode_wizard_artifact_fetch`; install catalog-only artifacts before fetching their bodies.'
880
1016
  }, null, 2),
881
1017
  metadata: {
882
1018
  status: 'ready',
@@ -887,17 +1023,24 @@ export const OpencodeWizardSkillsPlugin = async input => {
887
1023
  }
888
1024
  };
889
1025
  }
890
- if (selection.selectedItems.length === 0 && requestedArtifacts.length === 1) {
1026
+ if (selection.selectedItems.length === 0) {
891
1027
  return {
892
1028
  output: JSON.stringify({
893
1029
  pluginId: PLUGIN_ID,
894
1030
  runtimeMode: 'tool_fetch_only',
895
1031
  status: 'not_found',
896
1032
  artifactKind,
897
- requestedArtifact: requestedArtifacts[0],
1033
+ requestedArtifact: requestedArtifacts.length === 1 ? requestedArtifacts[0] : undefined,
1034
+ requestedArtifacts,
1035
+ missingArtifacts: selection.missingIdentifiers,
1036
+ recommendationContext,
1037
+ recommendationNote: RECOMMENDATION_METADATA_NOTE,
1038
+ suggestions: requestedArtifacts.length === 1 ? getIdentifierSuggestions(requestedArtifacts[0] ?? '', artifactSuggestionCandidates) : undefined,
1039
+ missingArtifactSuggestions: getMissingIdentifierSuggestions(selection.missingIdentifiers, artifactSuggestionCandidates),
898
1040
  availableArtifacts: fetchResult.payload.artifacts.map(toWizardArtifactSummary),
899
1041
  requestedDirectoryPath: directoryPath,
900
- workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution)
1042
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
1043
+ nextStep: 'Retry with one suggested identifier, fetch the artifact catalog to inspect available artifacts, or install a catalog-only suggestion before fetching its body.'
901
1044
  }, null, 2),
902
1045
  metadata: {
903
1046
  status: 'not_found',
@@ -957,6 +1100,10 @@ export const OpencodeWizardSkillsPlugin = async input => {
957
1100
  cacheTtlMs: CACHE_TTL_MS,
958
1101
  requestedArtifacts,
959
1102
  missingArtifacts: selection.missingIdentifiers,
1103
+ recommendationContext,
1104
+ recommendationNote: RECOMMENDATION_METADATA_NOTE,
1105
+ missingArtifactSuggestions: getMissingIdentifierSuggestions(selection.missingIdentifiers, artifactSuggestionCandidates),
1106
+ nextStep: selection.missingIdentifiers.length > 0 ? 'Fetched matched artifacts. Retry missing identifiers with suggested active names, or inspect the catalog for catalog-only artifacts that need installation first.' : 'Use the fetched artifact markdown as reference content for matched backend-published artifacts.',
960
1107
  artifacts: details
961
1108
  }, null, 2),
962
1109
  metadata: {
@@ -973,7 +1120,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
973
1120
  skill: args.artifact,
974
1121
  skills: args.artifacts,
975
1122
  directory: args.directory,
976
- refresh: args.refresh
1123
+ refresh: args.refresh,
1124
+ recommendationContext: args.recommendationContext
977
1125
  },
978
1126
  context
979
1127
  });
@@ -1043,7 +1191,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
1043
1191
  const catalogResult = await loadPublishedSkillCatalog({
1044
1192
  directory: requestedDirectory,
1045
1193
  useCache: true,
1046
- signal: context.abort
1194
+ signal: context.abort,
1195
+ recommendationContext: null
1047
1196
  });
1048
1197
  if (!catalogResult.fetchResult.ok) {
1049
1198
  await emitPreferenceOutcome('PREFERENCE_FAILED');
@@ -1559,7 +1708,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
1559
1708
  let publishedSkillsResult = await loadPublishedSkillCatalog({
1560
1709
  directory: input.directory,
1561
1710
  useCache: true,
1562
- signal: AbortSignal.timeout(5_000)
1711
+ signal: AbortSignal.timeout(5_000),
1712
+ recommendationContext: null
1563
1713
  });
1564
1714
  if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
1565
1715
  try {
@@ -1569,7 +1719,8 @@ export const OpencodeWizardSkillsPlugin = async input => {
1569
1719
  publishedSkillsResult = await loadPublishedSkillCatalog({
1570
1720
  directory: input.directory,
1571
1721
  useCache: false,
1572
- signal: AbortSignal.timeout(5_000)
1722
+ signal: AbortSignal.timeout(5_000),
1723
+ recommendationContext: null
1573
1724
  });
1574
1725
  } catch {
1575
1726
  const loginMessage = loginBootstrap.snapshot.message ? ` Last login status: ${loginBootstrap.snapshot.message}` : '';