@aexol/opencode-wizard 0.1.12 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import http from 'node:http';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import crypto from 'node:crypto';
5
6
  import { execFile } from 'node:child_process';
@@ -11,7 +12,8 @@ const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
11
12
  export const PLUGIN_ID = 'opencode-wizard';
12
13
  const CACHE_TTL_MS = 30_000;
13
14
  const ROOT_SKILL_SEED_PATH = '.opencode/skills';
14
- const AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
15
+ const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode-wizard.json');
16
+ const LEGACY_AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
15
17
  const PUBLISHED_BACKEND_ORIGIN = 'https://opencode-wizard.aexol.work';
16
18
  const OIDC_ISSUER = 'https://login.microsoftonline.com/86f4caf4-0d6f-4682-9a06-ea57f3e4e76c/v2.0';
17
19
  const OIDC_CLIENT_ID = 'da963901-2375-442b-9e99-14e59f43eda2';
@@ -52,6 +54,7 @@ const statusPathLoginBootstrap = {
52
54
  };
53
55
  const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
54
56
  export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch', 'opencode_wizard_status'];
57
+ let publishedSkillPreferenceCacheVersion = 0;
55
58
  export const NATIVE_SKILLS_URL_COMPATIBILITY = {
56
59
  configKey: 'skills.urls',
57
60
  deliveryMode: 'public_static_registry',
@@ -112,6 +115,137 @@ const PUBLISHED_SKILLS_CATALOG_QUERY = `
112
115
  publishedAt
113
116
  }
114
117
  }
118
+ catalogSkills {
119
+ skill {
120
+ id
121
+ slug
122
+ name
123
+ summary
124
+ whenToUse
125
+ status
126
+ installPolicy
127
+ tags {
128
+ id
129
+ slug
130
+ label
131
+ description
132
+ facet {
133
+ id
134
+ slug
135
+ label
136
+ description
137
+ }
138
+ }
139
+ }
140
+ skillVersion {
141
+ id
142
+ version
143
+ title
144
+ summary
145
+ status
146
+ }
147
+ publishedArtifact {
148
+ id
149
+ frontmatterName
150
+ frontmatterDescription
151
+ checksum
152
+ publishedAt
153
+ }
154
+ }
155
+ userPreferences {
156
+ scopeKey
157
+ userKey
158
+ ignoredSkills {
159
+ assignmentSource
160
+ assignmentType
161
+ scopePath
162
+ includeChildren
163
+ skill {
164
+ id
165
+ slug
166
+ name
167
+ summary
168
+ whenToUse
169
+ status
170
+ installPolicy
171
+ tags {
172
+ id
173
+ slug
174
+ label
175
+ description
176
+ facet {
177
+ id
178
+ slug
179
+ label
180
+ description
181
+ }
182
+ }
183
+ }
184
+ skillVersion {
185
+ id
186
+ version
187
+ title
188
+ summary
189
+ status
190
+ }
191
+ publishedArtifact {
192
+ id
193
+ frontmatterName
194
+ frontmatterDescription
195
+ checksum
196
+ publishedAt
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ `;
203
+ const SET_PUBLISHED_SKILL_PREFERENCE_MUTATION = `
204
+ mutation SetPublishedSkillPreference($input: SetPublishedSkillPreferenceInput!) {
205
+ setPublishedSkillPreference(input: $input) {
206
+ scopeKey
207
+ userKey
208
+ ignoredSkills {
209
+ assignmentSource
210
+ assignmentType
211
+ scopePath
212
+ includeChildren
213
+ skill {
214
+ id
215
+ slug
216
+ name
217
+ summary
218
+ whenToUse
219
+ status
220
+ installPolicy
221
+ tags {
222
+ id
223
+ slug
224
+ label
225
+ description
226
+ facet {
227
+ id
228
+ slug
229
+ label
230
+ description
231
+ }
232
+ }
233
+ }
234
+ skillVersion {
235
+ id
236
+ version
237
+ title
238
+ summary
239
+ status
240
+ }
241
+ publishedArtifact {
242
+ id
243
+ frontmatterName
244
+ frontmatterDescription
245
+ checksum
246
+ publishedAt
247
+ }
248
+ }
115
249
  }
116
250
  }
117
251
  `;
@@ -208,7 +342,7 @@ export const resolveConfig = async worktree => {
208
342
  actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
209
343
  fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
210
344
  rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
211
- authStatePath: AUTH_STATE_PATH
345
+ authStatePath: GLOBAL_CONFIG_PATH
212
346
  };
213
347
  };
214
348
  const normalizeAbsolutePath = value => path.resolve(value);
@@ -332,15 +466,89 @@ const isAuthState = value => {
332
466
  if (!isRecord(value)) return false;
333
467
  return value.pluginId === PLUGIN_ID && typeof value.sessionToken === 'string' && isValidIsoDateString(value.expiresAt) && isValidIsoDateString(value.authenticatedAt) && typeof value.userId === 'string' && typeof value.email === 'string';
334
468
  };
335
- const readAuthState = async authStateFile => {
469
+ const readGlobalConfig = async configFile => {
470
+ const storedConfig = await readJsonFile(configFile);
471
+ if (isRecord(storedConfig)) return storedConfig;
472
+ return {};
473
+ };
474
+ const writeGlobalConfig = async (configFile, config) => {
475
+ await writeJsonFile(configFile, config);
476
+ };
477
+ const withoutLegacyPublishedSkillPreferences = config => {
478
+ const {
479
+ publishedSkillPreferences,
480
+ ignoredPublishedSkills,
481
+ ...safeConfig
482
+ } = config;
483
+ void publishedSkillPreferences;
484
+ void ignoredPublishedSkills;
485
+ return safeConfig;
486
+ };
487
+ const hasLegacyPublishedSkillPreferences = config => {
488
+ return Object.prototype.hasOwnProperty.call(config, 'publishedSkillPreferences') || Object.prototype.hasOwnProperty.call(config, 'ignoredPublishedSkills');
489
+ };
490
+ const readGlobalAuthState = async configFile => {
491
+ const storedConfig = await readGlobalConfig(configFile);
492
+ const storedAuthState = storedConfig.auth;
493
+ if (storedAuthState === undefined || storedAuthState === null) return null;
494
+ if (isAuthState(storedAuthState)) {
495
+ if (hasLegacyPublishedSkillPreferences(storedConfig)) {
496
+ await writeGlobalConfig(configFile, withoutLegacyPublishedSkillPreferences(storedConfig));
497
+ }
498
+ return storedAuthState;
499
+ }
500
+ await writeGlobalConfig(configFile, {
501
+ ...withoutLegacyPublishedSkillPreferences(storedConfig),
502
+ auth: null
503
+ });
504
+ return null;
505
+ };
506
+ const readLegacyAuthState = async authStateFile => {
336
507
  const storedAuthState = await readJsonFile(authStateFile);
337
508
  if (storedAuthState === null) return null;
338
509
  if (isAuthState(storedAuthState)) return storedAuthState;
339
510
  await deleteFileIfExists(authStateFile);
340
511
  return null;
341
512
  };
342
- const writeAuthState = async (authStateFile, authState) => {
343
- await writeJsonFile(authStateFile, authState);
513
+ const writeAuthState = async (configFile, authState) => {
514
+ const storedConfig = await readGlobalConfig(configFile);
515
+ await writeGlobalConfig(configFile, {
516
+ ...withoutLegacyPublishedSkillPreferences(storedConfig),
517
+ auth: authState
518
+ });
519
+ };
520
+ const clearAuthState = async configFile => {
521
+ const storedConfig = await readGlobalConfig(configFile);
522
+ await writeGlobalConfig(configFile, {
523
+ ...withoutLegacyPublishedSkillPreferences(storedConfig),
524
+ auth: null
525
+ });
526
+ };
527
+ const toIgnoredSkillSlug = value => {
528
+ const normalized = value.trim().toLowerCase();
529
+ if (!normalized) return null;
530
+ return normalized;
531
+ };
532
+ const getPublishedSkillIgnoreScopeKey = (resolution, payload) => {
533
+ const workspaceSlug = payload?.workspace?.slug ?? resolution.fallbackWorkspaceSlug;
534
+ if (workspaceSlug) return `workspace:${toWorkspaceSlug(workspaceSlug)}`;
535
+ if (resolution.repositoryUrl) return `repository:${resolution.repositoryUrl}`;
536
+ return `path:${toWorkspaceSlug(path.basename(resolution.repositoryRoot))}`;
537
+ };
538
+ const toStoredUserKey = authState => {
539
+ if (authState?.userId) return authState.userId;
540
+ if (authState?.email) return authState.email.toLowerCase();
541
+ return 'anonymous';
542
+ };
543
+ const resolvePublishedSkillPreferenceCacheContext = async config => {
544
+ const authState = await readGlobalAuthState(config.authStatePath);
545
+ return {
546
+ userKey: toStoredUserKey(authState),
547
+ preferenceVersion: publishedSkillPreferenceCacheVersion
548
+ };
549
+ };
550
+ const getCatalogCacheKey = (workspaceResolution, preferenceContext) => {
551
+ return JSON.stringify([workspaceResolution.cacheKey, preferenceContext.userKey, preferenceContext.preferenceVersion]);
344
552
  };
345
553
  const toAuthState = session => ({
346
554
  pluginId: PLUGIN_ID,
@@ -351,16 +559,24 @@ const toAuthState = session => ({
351
559
  email: session.user.email
352
560
  });
353
561
  const resolveStoredAuthState = async (worktree, config) => {
354
- const authStateFile = path.resolve(worktree, config.authStatePath);
355
- const authState = await readAuthState(authStateFile);
356
- if (!authState) {
562
+ const authState = await readGlobalAuthState(config.authStatePath);
563
+ if (authState && Date.parse(authState.expiresAt) > Date.now()) {
564
+ return authState;
565
+ }
566
+ if (authState) {
567
+ await clearAuthState(config.authStatePath);
357
568
  return null;
358
569
  }
359
- if (Date.parse(authState.expiresAt) > Date.now()) {
360
- return authState;
570
+ const legacyAuthStateFile = path.resolve(worktree, LEGACY_AUTH_STATE_PATH);
571
+ const legacyAuthState = await readLegacyAuthState(legacyAuthStateFile);
572
+ if (!legacyAuthState) return null;
573
+ if (Date.parse(legacyAuthState.expiresAt) <= Date.now()) {
574
+ await deleteFileIfExists(legacyAuthStateFile);
575
+ return null;
361
576
  }
362
- await deleteFileIfExists(authStateFile);
363
- return null;
577
+ await writeAuthState(config.authStatePath, legacyAuthState);
578
+ await deleteFileIfExists(legacyAuthStateFile);
579
+ return legacyAuthState;
364
580
  };
365
581
  export const buildSkillMarkdown = item => {
366
582
  const artifactBody = item.publishedArtifact.markdownBody.trim();
@@ -435,11 +651,12 @@ const getPublishedSkillAssignmentCounts = items => items.reduce((counts, item) =
435
651
  other: 0
436
652
  });
437
653
  const getSkillContextKind = item => {
438
- if (item.assignmentSource === 'GLOBAL') return 'global';
654
+ if (item.assignmentSource === 'GLOBAL' || item.assignmentSource === 'USER_GLOBAL') return 'global';
439
655
  return 'project';
440
656
  };
441
657
  const getSkillPolicyLabel = (policy, contextKind) => {
442
658
  if (policy === 'GLOBAL_CONTEXT') return 'GLOBAL_CONTEXT · active context only, not project-installable';
659
+ if (contextKind === 'installable') return 'PROJECT_INSTALLABLE · available to install';
443
660
  if (contextKind === 'global') return 'PROJECT_INSTALLABLE · active global assignment';
444
661
  return 'PROJECT_INSTALLABLE · active project/workspace assignment';
445
662
  };
@@ -474,10 +691,35 @@ export const toPublishedSkillDetail = item => ({
474
691
  markdownBody: item.publishedArtifact.markdownBody,
475
692
  renderedContent: item.publishedArtifact.renderedContent
476
693
  });
694
+ const toInstallableSkillSummary = item => ({
695
+ skillSlug: item.skill.slug,
696
+ skillName: item.skill.name,
697
+ artifactName: item.publishedArtifact.frontmatterName,
698
+ artifactDescription: item.publishedArtifact.frontmatterDescription,
699
+ whenToUse: item.skill.whenToUse ?? null,
700
+ version: item.skillVersion.version,
701
+ assignmentSource: 'CATALOG',
702
+ assignmentType: 'PATH',
703
+ scopePath: '',
704
+ includeChildren: true,
705
+ checksum: item.publishedArtifact.checksum,
706
+ publishedAt: item.publishedArtifact.publishedAt,
707
+ identifiers: getSkillIdentifiers({
708
+ ...item,
709
+ assignmentSource: 'CATALOG',
710
+ assignmentType: 'PATH',
711
+ scopePath: '',
712
+ includeChildren: true
713
+ }),
714
+ tags: item.skill.tags.map(toPublishedSkillTagSummary),
715
+ contextKind: 'installable',
716
+ installPolicy: item.skill.installPolicy,
717
+ policyLabel: getSkillPolicyLabel(item.skill.installPolicy, 'installable')
718
+ });
477
719
  export const toPublishedSkillCatalog = payload => ({
478
720
  pluginId: PLUGIN_ID,
479
721
  runtimeMode: 'tool_fetch_only',
480
- deliveryModel: 'backend_published_global_project_assignments',
722
+ deliveryModel: 'backend_published_installed_effective_skills',
481
723
  workspace: payload.workspace,
482
724
  directoryPath: payload.directoryPath,
483
725
  rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
@@ -487,6 +729,40 @@ export const toPublishedSkillCatalog = payload => ({
487
729
  facets: getPublishedSkillFacets(payload.skills),
488
730
  skills: payload.skills.map(toPublishedSkillSummary)
489
731
  });
732
+ const filterIgnoredPublishedSkills = async (config, result) => {
733
+ const authState = await readGlobalAuthState(config.authStatePath);
734
+ const userKey = toStoredUserKey(authState);
735
+ if (!result.fetchResult.ok) {
736
+ return {
737
+ ...result,
738
+ ignoreState: {
739
+ scopeKey: getPublishedSkillIgnoreScopeKey(result.workspaceResolution),
740
+ userKey,
741
+ ignoredSkillSlugs: [],
742
+ installedGlobalSkillSlugs: [],
743
+ installedWorkspaceSkillSlugs: []
744
+ },
745
+ ignoredSkills: []
746
+ };
747
+ }
748
+ const ignoredSkills = result.fetchResult.payload.userPreferences.ignoredSkills.map(toPublishedSkillSummary);
749
+ const ignoredSkillSlugs = ignoredSkills.map(skill => skill.skillSlug);
750
+ return {
751
+ ...result,
752
+ ignoreState: {
753
+ scopeKey: result.fetchResult.payload.userPreferences.scopeKey,
754
+ userKey: result.fetchResult.payload.userPreferences.userKey || userKey,
755
+ ignoredSkillSlugs,
756
+ installedGlobalSkillSlugs: [],
757
+ installedWorkspaceSkillSlugs: []
758
+ },
759
+ ignoredSkills
760
+ };
761
+ };
762
+ const getWorkspaceUnavailableMessage = payload => {
763
+ if (payload.workspace) return null;
764
+ return 'Workspace-specific skills are unavailable because the workspace was not found; global skills are still loaded.';
765
+ };
490
766
  const normalizeSkillIdentifier = value => value.trim().toLowerCase();
491
767
  const parseSkillIdentifiers = value => {
492
768
  const seen = new Set();
@@ -568,7 +844,7 @@ const buildSystemNote = (result, config, details) => {
568
844
  const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project').slice(0, 5).map(buildSkillCatalogLine);
569
845
  const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
570
846
  const detailBlock = detailLines.length > 0 ? ` Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
571
- return [`opencode-wizard published skills are available from backend runtime delivery for workspace ${result.fetchResult.payload.workspace.slug}.`, `Current directory: ${result.directoryPath}.`, `Published skills for this scope: ${renderedSkillNames}${renderedCountSuffix}; counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.other} other.`, 'Catalog lines use short whenToUse guidance when available; fetch the full skill only when that guidance matches the task.', 'GLOBAL_CONTEXT skills are active context skills and are not project-installable; PROJECT_INSTALLABLE skills can be assigned globally or to project/workspace scopes; assignment rows decide which skills are active here.', globalSkills.length > 0 ? `Global context skills:\n${globalSkills.join('\n')}` : 'Global context skills: none.', projectSkills.length > 0 ? `Project-scoped active skills:\n${projectSkills.join('\n')}` : 'Project-scoped active skills: none.', detailBlock, 'Use opencode_wizard_published_skills_fetch for one or multiple skills.', `Root source seed path remains non-runtime input only: ${config.rootSkillSeedPath}/**.`].filter(line => line.length > 0).join(' ');
847
+ return [result.fetchResult.payload.workspace ? `opencode-wizard published skills are available from backend runtime delivery for workspace ${result.fetchResult.payload.workspace.slug}.` : 'opencode-wizard published global skills are available from backend runtime delivery; workspace-specific skills are unavailable because the workspace was not found.', `Current directory: ${result.directoryPath}.`, `Published skills for this scope: ${renderedSkillNames}${renderedCountSuffix}; counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.other} other.`, 'Catalog lines use short whenToUse guidance when available; fetch the full skill only when that guidance matches the task.', 'GLOBAL_CONTEXT skills are active context skills and are not project-installable; PROJECT_INSTALLABLE skills can be assigned globally or to project/workspace scopes; assignment rows decide which skills are active here.', globalSkills.length > 0 ? `Global context skills:\n${globalSkills.join('\n')}` : 'Global context skills: none.', projectSkills.length > 0 ? `Project-scoped active skills:\n${projectSkills.join('\n')}` : 'Project-scoped active skills: none.', detailBlock, 'Use opencode_wizard_published_skills_fetch for one or multiple skills.', `Root source seed path remains non-runtime input only: ${config.rootSkillSeedPath}/**.`].filter(line => line.length > 0).join(' ');
572
848
  };
573
849
  const toWorkspaceResolutionOutput = resolution => ({
574
850
  requestedDirectory: resolution.requestedDirectory,
@@ -585,6 +861,7 @@ const toWorkspaceResolutionMetadata = resolution => ({
585
861
  });
586
862
  const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
587
863
  const authState = await resolveStoredAuthState(worktree, config);
864
+ const filteredResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
588
865
  const base = {
589
866
  pluginId: PLUGIN_ID,
590
867
  runtimeMode: 'tool_fetch_only',
@@ -613,20 +890,26 @@ const formatStatusOutput = async (worktree, config, publishedSkillsResult, login
613
890
  email: loginBootstrapSnapshot.email,
614
891
  message: loginBootstrapSnapshot.message
615
892
  },
616
- status: publishedSkillsResult.fetchResult.status,
617
- fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
618
- source: publishedSkillsResult.fetchResult.source,
619
- availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS
893
+ status: filteredResult.fetchResult.status,
894
+ fetchedAt: filteredResult.fetchResult.fetchedAt,
895
+ source: filteredResult.fetchResult.source,
896
+ availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
897
+ ignoredPublishedSkills: {
898
+ scopeKey: filteredResult.ignoreState.scopeKey,
899
+ userKey: filteredResult.ignoreState.userKey,
900
+ count: filteredResult.ignoreState.ignoredSkillSlugs.length
901
+ }
620
902
  };
621
- if (!publishedSkillsResult.fetchResult.ok) {
903
+ if (!filteredResult.fetchResult.ok) {
622
904
  return JSON.stringify({
623
905
  ...base,
624
- message: publishedSkillsResult.fetchResult.message
906
+ message: filteredResult.fetchResult.message
625
907
  }, null, 2);
626
908
  }
627
909
  return JSON.stringify({
628
910
  ...base,
629
- ...toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload)
911
+ ...toPublishedSkillCatalog(filteredResult.fetchResult.payload),
912
+ message: getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload)
630
913
  }, null, 2);
631
914
  };
632
915
  export const toPluginAuthStateSummary = authState => {
@@ -658,6 +941,11 @@ export const resolvePluginStatusSnapshot = async ({
658
941
  directory
659
942
  });
660
943
  const fetchResult = await fetchPublishedSkillsCatalog(worktree, config, workspaceResolution, signal);
944
+ const filteredResult = await filterIgnoredPublishedSkills(config, {
945
+ directoryPath: workspaceResolution.directoryPath,
946
+ workspaceResolution,
947
+ fetchResult
948
+ });
661
949
  const authState = await resolveStoredAuthState(worktree, config);
662
950
  return {
663
951
  pluginId: PLUGIN_ID,
@@ -670,19 +958,43 @@ export const resolvePluginStatusSnapshot = async ({
670
958
  rootSkillSeedPath: config.rootSkillSeedPath,
671
959
  authStatePath: config.authStatePath,
672
960
  authState: toPluginAuthStateSummary(authState),
673
- status: fetchResult.status,
674
- authMode: fetchResult.authMode,
675
- fetchedAt: fetchResult.fetchedAt,
676
- source: fetchResult.source,
961
+ status: filteredResult.fetchResult.status,
962
+ authMode: filteredResult.fetchResult.authMode,
963
+ fetchedAt: filteredResult.fetchResult.fetchedAt,
964
+ source: filteredResult.fetchResult.source,
677
965
  availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
678
- message: fetchResult.ok ? null : fetchResult.message,
679
- catalog: fetchResult.ok ? toPublishedSkillCatalog(fetchResult.payload) : null
966
+ message: filteredResult.fetchResult.ok ? getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload) : filteredResult.fetchResult.message,
967
+ catalog: filteredResult.fetchResult.ok ? toPublishedSkillCatalog(filteredResult.fetchResult.payload) : null,
968
+ installableCatalog: filteredResult.fetchResult.ok ? {
969
+ count: filteredResult.fetchResult.payload.catalogSkills.length,
970
+ skills: filteredResult.fetchResult.payload.catalogSkills.map(toInstallableSkillSummary)
971
+ } : null,
972
+ ignoredPublishedSkills: {
973
+ scopeKey: filteredResult.ignoreState.scopeKey,
974
+ userKey: filteredResult.ignoreState.userKey,
975
+ count: filteredResult.ignoreState.ignoredSkillSlugs.length,
976
+ skills: filteredResult.ignoredSkills
977
+ }
680
978
  };
681
979
  };
682
980
  const withStatusMessage = (snapshot, message) => ({
683
981
  ...snapshot,
684
982
  message
685
983
  });
984
+ const toAiFacingPluginStatusSnapshot = snapshot => {
985
+ const {
986
+ ignoredPublishedSkills,
987
+ installableCatalog: _installableCatalog,
988
+ ...safeSnapshot
989
+ } = snapshot;
990
+ return {
991
+ ...safeSnapshot,
992
+ ignoredPublishedSkills: {
993
+ scopeKey: ignoredPublishedSkills.scopeKey,
994
+ count: ignoredPublishedSkills.count
995
+ }
996
+ };
997
+ };
686
998
  const startStatusPathLoginBootstrap = (worktree, config) => {
687
999
  if (statusPathLoginBootstrap.promise) return;
688
1000
  if (statusPathLoginBootstrap.status === 'failed' && statusPathLoginBootstrap.failedAt && Date.now() - statusPathLoginBootstrap.failedAt < STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS) {
@@ -714,7 +1026,7 @@ const startStatusPathLoginBootstrap = (worktree, config) => {
714
1026
  signal: loginSignal
715
1027
  });
716
1028
  const authState = toAuthState(pluginSession);
717
- await writeAuthState(path.resolve(worktree, config.authStatePath), authState);
1029
+ await writeAuthState(config.authStatePath, authState);
718
1030
  statusPathLoginBootstrap.status = 'authenticated';
719
1031
  statusPathLoginBootstrap.message = `Browser login completed successfully for ${authState.email}.`;
720
1032
  return authState;
@@ -749,6 +1061,93 @@ export const resolvePluginStatusSnapshotWithAuthBootstrap = async ({
749
1061
  }
750
1062
  return withStatusMessage(snapshot, 'Browser login is pending from the TUI/status path.');
751
1063
  };
1064
+ const toBackendPreferenceScope = preferenceScope => {
1065
+ if (preferenceScope === 'global') return 'GLOBAL';
1066
+ return 'WORKSPACE';
1067
+ };
1068
+ const setPublishedSkillPreference = async ({
1069
+ worktree,
1070
+ directory,
1071
+ config,
1072
+ skillSlug,
1073
+ preferenceScope,
1074
+ installed,
1075
+ ignored
1076
+ }) => {
1077
+ const workspaceResolution = await resolveWorkspace({
1078
+ config,
1079
+ directory
1080
+ });
1081
+ const response = await fetchPublishedSkillsGraphQl({
1082
+ worktree,
1083
+ config,
1084
+ query: SET_PUBLISHED_SKILL_PREFERENCE_MUTATION,
1085
+ variables: {
1086
+ input: {
1087
+ ...toDeliveryInput(workspaceResolution),
1088
+ skillSlug,
1089
+ preferenceScope: toBackendPreferenceScope(preferenceScope),
1090
+ installed,
1091
+ ignored
1092
+ }
1093
+ },
1094
+ signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
1095
+ });
1096
+ if (!response.ok) {
1097
+ throw new Error(response.result.message);
1098
+ }
1099
+ const preferences = response.data.setPublishedSkillPreference;
1100
+ publishedSkillPreferenceCacheVersion += 1;
1101
+ return {
1102
+ scopeKey: preferences.scopeKey,
1103
+ userKey: preferences.userKey,
1104
+ ignoredSkillSlugs: preferences.ignoredSkills.map(item => item.skill.slug),
1105
+ installedGlobalSkillSlugs: [],
1106
+ installedWorkspaceSkillSlugs: []
1107
+ };
1108
+ };
1109
+ export const setPublishedSkillIgnored = async ({
1110
+ worktree,
1111
+ directory,
1112
+ skillSlug,
1113
+ ignored,
1114
+ preferenceScope
1115
+ }) => {
1116
+ const config = await resolveConfig(worktree);
1117
+ const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
1118
+ if (!normalizedSkillSlug) {
1119
+ throw new Error('Cannot toggle an empty published skill slug.');
1120
+ }
1121
+ return setPublishedSkillPreference({
1122
+ worktree,
1123
+ directory,
1124
+ config,
1125
+ skillSlug: normalizedSkillSlug,
1126
+ preferenceScope: preferenceScope ?? 'project',
1127
+ ignored
1128
+ });
1129
+ };
1130
+ export const setPublishedSkillInstalled = async ({
1131
+ worktree,
1132
+ directory,
1133
+ skillSlug,
1134
+ installed,
1135
+ preferenceScope
1136
+ }) => {
1137
+ const config = await resolveConfig(worktree);
1138
+ const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
1139
+ if (!normalizedSkillSlug) {
1140
+ throw new Error('Cannot toggle an empty published skill slug.');
1141
+ }
1142
+ return setPublishedSkillPreference({
1143
+ worktree,
1144
+ directory,
1145
+ config,
1146
+ skillSlug: normalizedSkillSlug,
1147
+ preferenceScope,
1148
+ installed
1149
+ });
1150
+ };
752
1151
  const toPluginStatusMetadata = snapshot => ({
753
1152
  backendOrigin: snapshot.backendOrigin,
754
1153
  graphqlUrl: snapshot.graphqlUrl,
@@ -791,12 +1190,204 @@ const fetchOidcDiscoveryDocument = async signal => {
791
1190
  }
792
1191
  return await response.json();
793
1192
  };
1193
+ const isCallbackPortInUseError = error => {
1194
+ if (!error || typeof error !== 'object') return false;
1195
+ if (!('code' in error)) return false;
1196
+ return error.code === 'EADDRINUSE';
1197
+ };
1198
+ const toCallbackServerStartError = error => {
1199
+ if (!isCallbackPortInUseError(error)) {
1200
+ return error instanceof Error ? error : new Error('Failed to start local OAuth callback server.');
1201
+ }
1202
+ return new Error('OAuth login cannot start because localhost:24953 is already in use. Another OpenCode login is likely in progress; finish it or close the other instance, then retry.');
1203
+ };
1204
+ const escapeHtml = value => {
1205
+ return value.replace(/[&<>'"]/g, character => {
1206
+ const replacements = {
1207
+ '&': '&amp;',
1208
+ '<': '&lt;',
1209
+ '>': '&gt;',
1210
+ "'": '&#39;',
1211
+ '"': '&quot;'
1212
+ };
1213
+ return replacements[character] ?? character;
1214
+ });
1215
+ };
794
1216
  const sendHtmlResponse = (response, statusCode, title, message) => {
1217
+ const escapedTitle = escapeHtml(title);
1218
+ const escapedMessage = escapeHtml(message);
1219
+ const isSuccess = statusCode >= 200 && statusCode < 300;
1220
+ const pageState = isSuccess ? 'success' : statusCode === 404 ? 'not-found' : 'error';
1221
+ const cardTitle = isSuccess ? 'Authorization successful' : statusCode === 404 ? 'Callback not found' : 'Authorization failed';
1222
+ const escapedCardTitle = escapeHtml(cardTitle);
1223
+ const eyebrow = isSuccess ? 'Authorization complete' : statusCode === 404 ? 'Callback route not found' : 'Authorization needs attention';
1224
+ const actionText = isSuccess ? 'This window will close automatically in a moment. You can also close it now and return to OpenCode.' : 'You can close this window and return to OpenCode to try again.';
1225
+ const autoCloseScript = isSuccess ? `<script>
1226
+ window.setTimeout(() => window.close(), 2000);
1227
+ </script>` : '';
1228
+ const stateIcon = isSuccess ? '<path d="M7 12.5l3.1 3.1L17.5 8" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>' : statusCode === 404 ? '<path d="M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z" stroke="currentColor" stroke-width="2.2"/><path d="m15.5 15.5 4 4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>' : '<path d="M12 7v6" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M12 17.2v.1" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>';
795
1229
  response.writeHead(statusCode, {
796
1230
  'content-type': 'text/html; charset=utf-8',
797
1231
  'cache-control': 'no-store'
798
1232
  });
799
- response.end(`<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><h1>${title}</h1><p>${message}</p><p>You can close this window and return to OpenCode.</p></body></html>`);
1233
+ response.end(`<!doctype html>
1234
+ <html lang="en">
1235
+ <head>
1236
+ <meta charset="utf-8">
1237
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1238
+ <meta name="color-scheme" content="light dark">
1239
+ <title>${escapedTitle}</title>
1240
+ <style>
1241
+ :root {
1242
+ color-scheme: light dark;
1243
+ --page-bg: #f2efe7;
1244
+ --page-ink: #211d18;
1245
+ --muted: #6c6258;
1246
+ --panel: rgba(255, 252, 245, 0.82);
1247
+ --panel-border: rgba(78, 66, 52, 0.16);
1248
+ --success: #167848;
1249
+ --error: #ba3329;
1250
+ --not-found: #986614;
1251
+ --glow: rgba(22, 120, 72, 0.18);
1252
+ }
1253
+
1254
+ @media (prefers-color-scheme: dark) {
1255
+ :root {
1256
+ --page-bg: #12100d;
1257
+ --page-ink: #f7efe2;
1258
+ --muted: #b8aa98;
1259
+ --panel: rgba(30, 26, 22, 0.78);
1260
+ --panel-border: rgba(255, 244, 224, 0.14);
1261
+ --success: #71e0a6;
1262
+ --error: #ff897e;
1263
+ --not-found: #f7c96f;
1264
+ --glow: rgba(113, 224, 166, 0.2);
1265
+ }
1266
+ }
1267
+
1268
+ * {
1269
+ box-sizing: border-box;
1270
+ }
1271
+
1272
+ body {
1273
+ min-height: 100vh;
1274
+ margin: 0;
1275
+ display: grid;
1276
+ place-items: center;
1277
+ padding: 24px;
1278
+ overflow: hidden;
1279
+ background:
1280
+ radial-gradient(circle at 18% 18%, var(--glow), transparent 34rem),
1281
+ radial-gradient(circle at 82% 12%, rgba(209, 142, 72, 0.18), transparent 30rem),
1282
+ linear-gradient(135deg, var(--page-bg), color-mix(in srgb, var(--page-bg) 76%, #000 24%));
1283
+ color: var(--page-ink);
1284
+ font-family: ui-rounded, "SF Pro Rounded", "Segoe UI", system-ui, sans-serif;
1285
+ }
1286
+
1287
+ body::before {
1288
+ content: "";
1289
+ position: fixed;
1290
+ inset: -20%;
1291
+ pointer-events: none;
1292
+ background-image:
1293
+ linear-gradient(rgba(128, 104, 74, 0.08) 1px, transparent 1px),
1294
+ linear-gradient(90deg, rgba(128, 104, 74, 0.08) 1px, transparent 1px);
1295
+ background-size: 42px 42px;
1296
+ mask-image: radial-gradient(circle at center, black, transparent 68%);
1297
+ }
1298
+
1299
+ main {
1300
+ position: relative;
1301
+ width: min(100%, 560px);
1302
+ padding: clamp(28px, 7vw, 56px);
1303
+ border: 1px solid var(--panel-border);
1304
+ border-radius: 32px;
1305
+ background: var(--panel);
1306
+ box-shadow: 0 24px 90px rgba(0, 0, 0, 0.24);
1307
+ text-align: center;
1308
+ backdrop-filter: blur(18px) saturate(1.2);
1309
+ }
1310
+
1311
+ .mark {
1312
+ width: 72px;
1313
+ height: 72px;
1314
+ margin: 0 auto 24px;
1315
+ display: grid;
1316
+ place-items: center;
1317
+ border-radius: 24px;
1318
+ color: var(--state-color);
1319
+ background: color-mix(in srgb, var(--state-color) 16%, transparent);
1320
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--state-color) 28%, transparent);
1321
+ }
1322
+
1323
+ [data-state="success"] { --state-color: var(--success); }
1324
+ [data-state="error"] { --state-color: var(--error); }
1325
+ [data-state="not-found"] { --state-color: var(--not-found); }
1326
+
1327
+ .eyebrow {
1328
+ margin: 0 0 10px;
1329
+ color: var(--state-color);
1330
+ font-size: 0.78rem;
1331
+ font-weight: 800;
1332
+ letter-spacing: 0.14em;
1333
+ text-transform: uppercase;
1334
+ }
1335
+
1336
+ h1 {
1337
+ margin: 0;
1338
+ font-size: clamp(2rem, 7vw, 3.35rem);
1339
+ line-height: 0.95;
1340
+ letter-spacing: -0.06em;
1341
+ }
1342
+
1343
+ .message {
1344
+ margin: 22px auto 0;
1345
+ max-width: 38rem;
1346
+ color: var(--muted);
1347
+ font-size: clamp(1rem, 2.5vw, 1.1rem);
1348
+ line-height: 1.65;
1349
+ }
1350
+
1351
+ .next-step {
1352
+ margin: 26px 0 0;
1353
+ padding: 14px 18px;
1354
+ border-radius: 999px;
1355
+ background: color-mix(in srgb, var(--state-color) 12%, transparent);
1356
+ color: var(--page-ink);
1357
+ font-size: 0.94rem;
1358
+ line-height: 1.5;
1359
+ }
1360
+
1361
+ @media (max-width: 520px) {
1362
+ body {
1363
+ padding: 16px;
1364
+ }
1365
+
1366
+ main {
1367
+ border-radius: 24px;
1368
+ }
1369
+
1370
+ .next-step {
1371
+ border-radius: 18px;
1372
+ }
1373
+ }
1374
+ </style>
1375
+ </head>
1376
+ <body>
1377
+ <main data-state="${pageState}" aria-labelledby="callback-title">
1378
+ <div class="mark" aria-hidden="true">
1379
+ <svg width="34" height="34" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1380
+ ${stateIcon}
1381
+ </svg>
1382
+ </div>
1383
+ <p class="eyebrow">${eyebrow}</p>
1384
+ <h1 id="callback-title">${escapedCardTitle}</h1>
1385
+ <p class="message">${escapedMessage}</p>
1386
+ <p class="next-step">${actionText}</p>
1387
+ </main>
1388
+ ${autoCloseScript}
1389
+ </body>
1390
+ </html>`);
800
1391
  };
801
1392
  const startLocalCallbackServer = async ({
802
1393
  expectedState,
@@ -861,9 +1452,6 @@ const startLocalCallbackServer = async ({
861
1452
  state
862
1453
  });
863
1454
  });
864
- server.on('error', error => {
865
- fail(error instanceof Error ? error : new Error('Failed to start local OAuth callback server.'));
866
- });
867
1455
  const close = async () => {
868
1456
  await new Promise((resolve, reject) => {
869
1457
  server.close(error => {
@@ -876,8 +1464,17 @@ const startLocalCallbackServer = async ({
876
1464
  });
877
1465
  };
878
1466
  await new Promise((resolve, reject) => {
879
- server.listen(24953, 'localhost', () => resolve());
880
- server.once('error', reject);
1467
+ const rejectStart = error => {
1468
+ reject(toCallbackServerStartError(error));
1469
+ };
1470
+ server.once('error', rejectStart);
1471
+ server.listen(24953, 'localhost', () => {
1472
+ server.off('error', rejectStart);
1473
+ server.on('error', error => {
1474
+ fail(error instanceof Error ? error : new Error('Local OAuth callback server failed.'));
1475
+ });
1476
+ resolve();
1477
+ });
881
1478
  });
882
1479
  signal.addEventListener('abort', () => {
883
1480
  fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
@@ -941,7 +1538,7 @@ const fetchPublishedSkillsGraphQl = async ({
941
1538
  };
942
1539
  }
943
1540
  if (response.status === 401 || response.status === 403) {
944
- await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
1541
+ await clearAuthState(config.authStatePath);
945
1542
  onAuthStateChanged?.();
946
1543
  return {
947
1544
  ok: false,
@@ -987,7 +1584,7 @@ const fetchPublishedSkillsGraphQl = async ({
987
1584
  if (body.errors?.length) {
988
1585
  const message = body.errors.map(error => error.message).join('; ');
989
1586
  if (body.errors.some(error => isUnauthorizedGraphQlMessage(error.message))) {
990
- await deleteFileIfExists(path.resolve(worktree, config.authStatePath));
1587
+ await clearAuthState(config.authStatePath);
991
1588
  onAuthStateChanged?.();
992
1589
  return {
993
1590
  ok: false,
@@ -1260,11 +1857,11 @@ const openBrowser = async url => {
1260
1857
  const normalizeDirectoryArg = (contextDirectory, directory) => {
1261
1858
  return normalizeAbsolutePath(directory ? path.resolve(contextDirectory, directory) : contextDirectory);
1262
1859
  };
1263
- const getDetailCacheKey = (workspaceResolution, skillVersionId) => {
1264
- return JSON.stringify([workspaceResolution.cacheKey, skillVersionId]);
1860
+ const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
1861
+ return JSON.stringify([catalogCacheKey, skillVersionId]);
1265
1862
  };
1266
- const getDetailInflightKey = (workspaceResolution, skillVersionId, purpose) => {
1267
- return JSON.stringify([workspaceResolution.cacheKey, skillVersionId, purpose]);
1863
+ const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
1864
+ return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
1268
1865
  };
1269
1866
  const OpencodeWizardSkillsPlugin = async input => {
1270
1867
  const {
@@ -1276,7 +1873,6 @@ const OpencodeWizardSkillsPlugin = async input => {
1276
1873
  const catalogInflight = new Map();
1277
1874
  const detailCache = new Map();
1278
1875
  const detailInflight = new Map();
1279
- const authStateFile = path.resolve(input.worktree, config.authStatePath);
1280
1876
  const initialAuthState = await resolveStoredAuthState(input.worktree, config);
1281
1877
  const loginBootstrap = {
1282
1878
  promise: null,
@@ -1375,7 +1971,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1375
1971
  };
1376
1972
  const persistAuthState = async session => {
1377
1973
  const authState = toAuthState(session);
1378
- await writeAuthState(authStateFile, authState);
1974
+ await writeAuthState(config.authStatePath, authState);
1379
1975
  clearPublishedSkillState();
1380
1976
  return authState;
1381
1977
  };
@@ -1396,19 +1992,20 @@ const OpencodeWizardSkillsPlugin = async input => {
1396
1992
  };
1397
1993
  const loginPromise = (async () => {
1398
1994
  const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
1399
- const loginStart = await startLoginFlow(loginSignal);
1400
- const browserOpenError = await openBrowser(loginStart.browserUrl);
1401
- loginBootstrap.snapshot = {
1402
- status: 'pending',
1403
- trigger,
1404
- startedAt,
1405
- expiresAt: loginStart.expiresAt,
1406
- browserUrl: loginStart.browserUrl,
1407
- browserOpenError,
1408
- email: null,
1409
- message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
1410
- };
1995
+ let loginStart = null;
1411
1996
  try {
1997
+ loginStart = await startLoginFlow(loginSignal);
1998
+ const browserOpenError = await openBrowser(loginStart.browserUrl);
1999
+ loginBootstrap.snapshot = {
2000
+ status: 'pending',
2001
+ trigger,
2002
+ startedAt,
2003
+ expiresAt: loginStart.expiresAt,
2004
+ browserUrl: loginStart.browserUrl,
2005
+ browserOpenError,
2006
+ email: null,
2007
+ message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
2008
+ };
1412
2009
  const callbackPayload = await loginStart.callbackPromise;
1413
2010
  if (callbackPayload.status === 'error') {
1414
2011
  throw new Error(callbackPayload.message);
@@ -1467,7 +2064,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1467
2064
  };
1468
2065
  throw error;
1469
2066
  } finally {
1470
- await loginStart.closeCallbackServer().catch(() => undefined);
2067
+ await loginStart?.closeCallbackServer().catch(() => undefined);
1471
2068
  loginBootstrap.promise = null;
1472
2069
  }
1473
2070
  })();
@@ -1484,7 +2081,8 @@ const OpencodeWizardSkillsPlugin = async input => {
1484
2081
  directory
1485
2082
  });
1486
2083
  const directoryPath = workspaceResolution.directoryPath;
1487
- const cacheKey = workspaceResolution.cacheKey;
2084
+ const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
2085
+ const cacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
1488
2086
  const cached = cache.get(cacheKey);
1489
2087
  if (useCache && cached && cached.expiresAt > Date.now()) {
1490
2088
  return {
@@ -1527,8 +2125,10 @@ const OpencodeWizardSkillsPlugin = async input => {
1527
2125
  purpose
1528
2126
  }) => {
1529
2127
  const directoryPath = workspaceResolution.directoryPath;
1530
- const cacheKey = getDetailCacheKey(workspaceResolution, item.skillVersion.id);
1531
- const inflightKey = getDetailInflightKey(workspaceResolution, item.skillVersion.id, purpose);
2128
+ const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
2129
+ const catalogCacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
2130
+ const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
2131
+ const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
1532
2132
  const cached = detailCache.get(cacheKey);
1533
2133
  if (purpose === 'SYSTEM_CONTEXT' && useCache && cached && cached.expiresAt > Date.now()) {
1534
2134
  return {
@@ -1668,18 +2268,29 @@ const OpencodeWizardSkillsPlugin = async input => {
1668
2268
  loginBootstrapSnapshot: loginBootstrap.snapshot
1669
2269
  });
1670
2270
  }
1671
- const selection = selectPublishedSkills(publishedSkillsResult.fetchResult.payload, requestedSkills);
2271
+ const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
2272
+ if (!filteredPublishedSkillsResult.fetchResult.ok) {
2273
+ await emitFetchOutcome('FETCH_FAILED');
2274
+ return toFetchFailureOutput({
2275
+ worktree: input.worktree,
2276
+ config,
2277
+ publishedSkillsResult: filteredPublishedSkillsResult,
2278
+ loginBootstrapSnapshot: loginBootstrap.snapshot
2279
+ });
2280
+ }
2281
+ const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
1672
2282
  const isSingleRequest = requestedSkills.length === 1;
1673
2283
  if (requestedSkills.length === 0) {
1674
- const catalog = toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload);
2284
+ const catalog = toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload);
1675
2285
  context.metadata({
1676
- title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount}`,
2286
+ title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount} active`,
1677
2287
  metadata: {
1678
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2288
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
1679
2289
  status: 'ready',
1680
2290
  publishedSkillCount: catalog.publishedSkillCount.toString(),
1681
2291
  globalAssignmentCount: catalog.assignmentCounts.global.toString(),
1682
- projectAssignmentCount: catalog.assignmentCounts.project.toString()
2292
+ projectAssignmentCount: catalog.assignmentCounts.project.toString(),
2293
+ ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
1683
2294
  }
1684
2295
  });
1685
2296
  await emitFetchOutcome('FETCH_SUCCESS');
@@ -1687,16 +2298,21 @@ const OpencodeWizardSkillsPlugin = async input => {
1687
2298
  output: JSON.stringify({
1688
2299
  ...catalog,
1689
2300
  status: 'ready',
1690
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
1691
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
1692
- fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
1693
- source: publishedSkillsResult.fetchResult.source,
2301
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2302
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2303
+ ignoredPublishedSkills: {
2304
+ scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
2305
+ count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
2306
+ },
2307
+ fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2308
+ source: filteredPublishedSkillsResult.fetchResult.source,
1694
2309
  message: 'Catalog discovery only. Provide `skill` or `skills` to fetch markdown bodies/details for selected skills.'
1695
2310
  }, null, 2),
1696
2311
  metadata: {
1697
2312
  status: 'ready',
1698
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
1699
- publishedSkillCount: catalog.publishedSkillCount.toString()
2313
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2314
+ publishedSkillCount: catalog.publishedSkillCount.toString(),
2315
+ ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
1700
2316
  }
1701
2317
  };
1702
2318
  }
@@ -1707,19 +2323,23 @@ const OpencodeWizardSkillsPlugin = async input => {
1707
2323
  pluginId: PLUGIN_ID,
1708
2324
  runtimeMode: 'tool_fetch_only',
1709
2325
  status: 'not_found',
1710
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
1711
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
2326
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2327
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
1712
2328
  requestedSkill: requestedSkills[0],
1713
- availableSkills: publishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary)
2329
+ availableSkills: filteredPublishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary),
2330
+ ignoredPublishedSkills: {
2331
+ scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
2332
+ count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
2333
+ }
1714
2334
  }, null, 2),
1715
2335
  metadata: {
1716
2336
  status: 'not_found',
1717
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution)
2337
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution)
1718
2338
  }
1719
2339
  };
1720
2340
  }
1721
2341
  let skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
1722
- workspaceResolution: publishedSkillsResult.workspaceResolution,
2342
+ workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
1723
2343
  item,
1724
2344
  signal: context.abort,
1725
2345
  useCache: !args.refresh,
@@ -1731,7 +2351,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1731
2351
  await schedulePresenceStart(authState);
1732
2352
  });
1733
2353
  skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
1734
- workspaceResolution: publishedSkillsResult.workspaceResolution,
2354
+ workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
1735
2355
  item,
1736
2356
  signal: context.abort,
1737
2357
  useCache: false,
@@ -1757,7 +2377,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1757
2377
  context.metadata({
1758
2378
  title: `opencode-wizard published skill: ${detail.artifactName || detail.skillName}`,
1759
2379
  metadata: {
1760
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2380
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
1761
2381
  skillSlug: detail.skillSlug,
1762
2382
  version: detail.version
1763
2383
  }
@@ -1767,16 +2387,16 @@ const OpencodeWizardSkillsPlugin = async input => {
1767
2387
  output: JSON.stringify({
1768
2388
  pluginId: PLUGIN_ID,
1769
2389
  runtimeMode: 'tool_fetch_only',
1770
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
1771
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
1772
- workspace: publishedSkillsResult.fetchResult.payload.workspace,
1773
- fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
1774
- source: publishedSkillsResult.fetchResult.source,
2390
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2391
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2392
+ workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
2393
+ fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2394
+ source: filteredPublishedSkillsResult.fetchResult.source,
1775
2395
  skill: detail
1776
2396
  }, null, 2),
1777
2397
  metadata: {
1778
2398
  status: 'ready',
1779
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2399
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
1780
2400
  skillSlug: detail.skillSlug
1781
2401
  }
1782
2402
  };
@@ -1784,7 +2404,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1784
2404
  context.metadata({
1785
2405
  title: `opencode-wizard published skills fetch: ${skillDetails.length}`,
1786
2406
  metadata: {
1787
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2407
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
1788
2408
  requestedCount: requestedSkills.length.toString(),
1789
2409
  matchedCount: skillDetails.length.toString()
1790
2410
  }
@@ -1794,18 +2414,18 @@ const OpencodeWizardSkillsPlugin = async input => {
1794
2414
  output: JSON.stringify({
1795
2415
  pluginId: PLUGIN_ID,
1796
2416
  runtimeMode: 'tool_fetch_only',
1797
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
1798
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
1799
- workspace: publishedSkillsResult.fetchResult.payload.workspace,
1800
- fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
1801
- source: publishedSkillsResult.fetchResult.source,
2417
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2418
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2419
+ workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
2420
+ fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2421
+ source: filteredPublishedSkillsResult.fetchResult.source,
1802
2422
  requestedSkills,
1803
2423
  missingSkills: selection.missingIdentifiers,
1804
2424
  skills: skillDetails
1805
2425
  }, null, 2),
1806
2426
  metadata: {
1807
2427
  status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
1808
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2428
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
1809
2429
  matchedCount: skillDetails.length.toString()
1810
2430
  }
1811
2431
  };
@@ -1840,7 +2460,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1840
2460
  metadata
1841
2461
  });
1842
2462
  return {
1843
- output: JSON.stringify(snapshot, null, 2),
2463
+ output: JSON.stringify(toAiFacingPluginStatusSnapshot(snapshot), null, 2),
1844
2464
  metadata
1845
2465
  };
1846
2466
  };
@@ -1900,11 +2520,12 @@ const OpencodeWizardSkillsPlugin = async input => {
1900
2520
  return;
1901
2521
  }
1902
2522
  }
2523
+ const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
1903
2524
  const details = await loadSystemNoteDetails({
1904
- publishedSkillsResult,
2525
+ publishedSkillsResult: filteredPublishedSkillsResult,
1905
2526
  signal: AbortSignal.timeout(5_000)
1906
2527
  });
1907
- const systemNote = buildSystemNote(publishedSkillsResult, config, details);
2528
+ const systemNote = buildSystemNote(filteredPublishedSkillsResult, config, details);
1908
2529
  if (!systemNote) return;
1909
2530
  output.system.push(systemNote);
1910
2531
  }