@aexol/opencode-wizard 0.1.14 → 0.1.16

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
@@ -6,6 +6,8 @@ import crypto from 'node:crypto';
6
6
  import { execFile } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
8
8
  import { URL, fileURLToPath } from 'node:url';
9
+ import { resolveBackendOriginFromValues } from './config.js';
10
+ import { deleteFileIfExists, readJsonFile, writePrivateJsonFile } from './storage.js';
9
11
  const execFileAsync = promisify(execFile);
10
12
  const MODULE_FILE_PATH = fileURLToPath(import.meta.url);
11
13
  const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
@@ -14,7 +16,6 @@ const CACHE_TTL_MS = 30_000;
14
16
  const ROOT_SKILL_SEED_PATH = '.opencode/skills';
15
17
  const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode-wizard.json');
16
18
  const LEGACY_AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
17
- const PUBLISHED_BACKEND_ORIGIN = 'https://opencode-wizard.aexol.work';
18
19
  const OIDC_ISSUER = 'https://login.microsoftonline.com/86f4caf4-0d6f-4682-9a06-ea57f3e4e76c/v2.0';
19
20
  const OIDC_CLIENT_ID = 'da963901-2375-442b-9e99-14e59f43eda2';
20
21
  const OIDC_CALLBACK_ORIGIN = 'http://localhost:24953';
@@ -54,6 +55,7 @@ const statusPathLoginBootstrap = {
54
55
  };
55
56
  const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
56
57
  export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch', 'opencode_wizard_status'];
58
+ let publishedSkillPreferenceCacheVersion = 0;
57
59
  export const NATIVE_SKILLS_URL_COMPATIBILITY = {
58
60
  configKey: 'skills.urls',
59
61
  deliveryMode: 'public_static_registry',
@@ -114,6 +116,137 @@ const PUBLISHED_SKILLS_CATALOG_QUERY = `
114
116
  publishedAt
115
117
  }
116
118
  }
119
+ catalogSkills {
120
+ skill {
121
+ id
122
+ slug
123
+ name
124
+ summary
125
+ whenToUse
126
+ status
127
+ installPolicy
128
+ tags {
129
+ id
130
+ slug
131
+ label
132
+ description
133
+ facet {
134
+ id
135
+ slug
136
+ label
137
+ description
138
+ }
139
+ }
140
+ }
141
+ skillVersion {
142
+ id
143
+ version
144
+ title
145
+ summary
146
+ status
147
+ }
148
+ publishedArtifact {
149
+ id
150
+ frontmatterName
151
+ frontmatterDescription
152
+ checksum
153
+ publishedAt
154
+ }
155
+ }
156
+ userPreferences {
157
+ scopeKey
158
+ userKey
159
+ ignoredSkills {
160
+ assignmentSource
161
+ assignmentType
162
+ scopePath
163
+ includeChildren
164
+ skill {
165
+ id
166
+ slug
167
+ name
168
+ summary
169
+ whenToUse
170
+ status
171
+ installPolicy
172
+ tags {
173
+ id
174
+ slug
175
+ label
176
+ description
177
+ facet {
178
+ id
179
+ slug
180
+ label
181
+ description
182
+ }
183
+ }
184
+ }
185
+ skillVersion {
186
+ id
187
+ version
188
+ title
189
+ summary
190
+ status
191
+ }
192
+ publishedArtifact {
193
+ id
194
+ frontmatterName
195
+ frontmatterDescription
196
+ checksum
197
+ publishedAt
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ `;
204
+ const SET_PUBLISHED_SKILL_PREFERENCE_MUTATION = `
205
+ mutation SetPublishedSkillPreference($input: SetPublishedSkillPreferenceInput!) {
206
+ setPublishedSkillPreference(input: $input) {
207
+ scopeKey
208
+ userKey
209
+ ignoredSkills {
210
+ assignmentSource
211
+ assignmentType
212
+ scopePath
213
+ includeChildren
214
+ skill {
215
+ id
216
+ slug
217
+ name
218
+ summary
219
+ whenToUse
220
+ status
221
+ installPolicy
222
+ tags {
223
+ id
224
+ slug
225
+ label
226
+ description
227
+ facet {
228
+ id
229
+ slug
230
+ label
231
+ description
232
+ }
233
+ }
234
+ }
235
+ skillVersion {
236
+ id
237
+ version
238
+ title
239
+ summary
240
+ status
241
+ }
242
+ publishedArtifact {
243
+ id
244
+ frontmatterName
245
+ frontmatterDescription
246
+ checksum
247
+ publishedAt
248
+ }
249
+ }
117
250
  }
118
251
  }
119
252
  `;
@@ -175,20 +308,12 @@ const readLocalEnvValues = async startDirectory => {
175
308
  return new Map();
176
309
  }
177
310
  };
178
- const normalizeBackendOrigin = value => {
179
- if (!value) return null;
180
- try {
181
- const normalizedUrl = new URL(value);
182
- return normalizedUrl.origin;
183
- } catch {
184
- return null;
185
- }
186
- };
187
311
  const resolveBackendOrigin = async worktree => {
188
312
  const envValues = await readLocalEnvValues(worktree);
189
- const configuredBackendOrigin = normalizeBackendOrigin(process.env.OPENCODE_WIZARD_BACKEND_ORIGIN) ?? normalizeBackendOrigin(envValues.get('OPENCODE_WIZARD_BACKEND_ORIGIN'));
190
- if (configuredBackendOrigin) return configuredBackendOrigin;
191
- return PUBLISHED_BACKEND_ORIGIN;
313
+ return resolveBackendOriginFromValues({
314
+ environmentBackendOrigin: process.env.OPENCODE_WIZARD_BACKEND_ORIGIN,
315
+ localBackendOrigin: envValues.get('OPENCODE_WIZARD_BACKEND_ORIGIN')
316
+ });
192
317
  };
193
318
  const toWorkspaceSlug = value => {
194
319
  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '');
@@ -304,26 +429,6 @@ const formatSkillLabel = item => {
304
429
  return item.skill.name;
305
430
  };
306
431
  const toFrontmatterString = value => JSON.stringify(value);
307
- const readJsonFile = async filePath => {
308
- try {
309
- const raw = await fs.readFile(filePath, 'utf8');
310
- const parsed = JSON.parse(raw);
311
- return parsed;
312
- } catch {
313
- return null;
314
- }
315
- };
316
- const writeJsonFile = async (filePath, value) => {
317
- await fs.mkdir(path.dirname(filePath), {
318
- recursive: true
319
- });
320
- await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
321
- };
322
- const deleteFileIfExists = async filePath => {
323
- await fs.rm(filePath, {
324
- force: true
325
- });
326
- };
327
432
  const isRecord = value => {
328
433
  return typeof value === 'object' && value !== null && !Array.isArray(value);
329
434
  };
@@ -340,15 +445,33 @@ const readGlobalConfig = async configFile => {
340
445
  return {};
341
446
  };
342
447
  const writeGlobalConfig = async (configFile, config) => {
343
- await writeJsonFile(configFile, config);
448
+ await writePrivateJsonFile(configFile, config);
449
+ };
450
+ const withoutLegacyPublishedSkillPreferences = config => {
451
+ const {
452
+ publishedSkillPreferences,
453
+ ignoredPublishedSkills,
454
+ ...safeConfig
455
+ } = config;
456
+ void publishedSkillPreferences;
457
+ void ignoredPublishedSkills;
458
+ return safeConfig;
459
+ };
460
+ const hasLegacyPublishedSkillPreferences = config => {
461
+ return Object.prototype.hasOwnProperty.call(config, 'publishedSkillPreferences') || Object.prototype.hasOwnProperty.call(config, 'ignoredPublishedSkills');
344
462
  };
345
463
  const readGlobalAuthState = async configFile => {
346
464
  const storedConfig = await readGlobalConfig(configFile);
347
465
  const storedAuthState = storedConfig.auth;
348
466
  if (storedAuthState === undefined || storedAuthState === null) return null;
349
- if (isAuthState(storedAuthState)) return storedAuthState;
467
+ if (isAuthState(storedAuthState)) {
468
+ if (hasLegacyPublishedSkillPreferences(storedConfig)) {
469
+ await writeGlobalConfig(configFile, withoutLegacyPublishedSkillPreferences(storedConfig));
470
+ }
471
+ return storedAuthState;
472
+ }
350
473
  await writeGlobalConfig(configFile, {
351
- ...storedConfig,
474
+ ...withoutLegacyPublishedSkillPreferences(storedConfig),
352
475
  auth: null
353
476
  });
354
477
  return null;
@@ -363,17 +486,43 @@ const readLegacyAuthState = async authStateFile => {
363
486
  const writeAuthState = async (configFile, authState) => {
364
487
  const storedConfig = await readGlobalConfig(configFile);
365
488
  await writeGlobalConfig(configFile, {
366
- ...storedConfig,
489
+ ...withoutLegacyPublishedSkillPreferences(storedConfig),
367
490
  auth: authState
368
491
  });
369
492
  };
370
493
  const clearAuthState = async configFile => {
371
494
  const storedConfig = await readGlobalConfig(configFile);
372
495
  await writeGlobalConfig(configFile, {
373
- ...storedConfig,
496
+ ...withoutLegacyPublishedSkillPreferences(storedConfig),
374
497
  auth: null
375
498
  });
376
499
  };
500
+ const toIgnoredSkillSlug = value => {
501
+ const normalized = value.trim().toLowerCase();
502
+ if (!normalized) return null;
503
+ return normalized;
504
+ };
505
+ const getPublishedSkillIgnoreScopeKey = (resolution, payload) => {
506
+ const workspaceSlug = payload?.workspace?.slug ?? resolution.fallbackWorkspaceSlug;
507
+ if (workspaceSlug) return `workspace:${toWorkspaceSlug(workspaceSlug)}`;
508
+ if (resolution.repositoryUrl) return `repository:${resolution.repositoryUrl}`;
509
+ return `path:${toWorkspaceSlug(path.basename(resolution.repositoryRoot))}`;
510
+ };
511
+ const toStoredUserKey = authState => {
512
+ if (authState?.userId) return authState.userId;
513
+ if (authState?.email) return authState.email.toLowerCase();
514
+ return 'anonymous';
515
+ };
516
+ const resolvePublishedSkillPreferenceCacheContext = async config => {
517
+ const authState = await readGlobalAuthState(config.authStatePath);
518
+ return {
519
+ userKey: toStoredUserKey(authState),
520
+ preferenceVersion: publishedSkillPreferenceCacheVersion
521
+ };
522
+ };
523
+ const getCatalogCacheKey = (workspaceResolution, preferenceContext) => {
524
+ return JSON.stringify([workspaceResolution.cacheKey, preferenceContext.userKey, preferenceContext.preferenceVersion]);
525
+ };
377
526
  const toAuthState = session => ({
378
527
  pluginId: PLUGIN_ID,
379
528
  sessionToken: session.jwtToken,
@@ -475,11 +624,12 @@ const getPublishedSkillAssignmentCounts = items => items.reduce((counts, item) =
475
624
  other: 0
476
625
  });
477
626
  const getSkillContextKind = item => {
478
- if (item.assignmentSource === 'GLOBAL') return 'global';
627
+ if (item.assignmentSource === 'GLOBAL' || item.assignmentSource === 'USER_GLOBAL') return 'global';
479
628
  return 'project';
480
629
  };
481
630
  const getSkillPolicyLabel = (policy, contextKind) => {
482
631
  if (policy === 'GLOBAL_CONTEXT') return 'GLOBAL_CONTEXT · active context only, not project-installable';
632
+ if (contextKind === 'installable') return 'PROJECT_INSTALLABLE · available to install';
483
633
  if (contextKind === 'global') return 'PROJECT_INSTALLABLE · active global assignment';
484
634
  return 'PROJECT_INSTALLABLE · active project/workspace assignment';
485
635
  };
@@ -514,10 +664,35 @@ export const toPublishedSkillDetail = item => ({
514
664
  markdownBody: item.publishedArtifact.markdownBody,
515
665
  renderedContent: item.publishedArtifact.renderedContent
516
666
  });
667
+ const toInstallableSkillSummary = item => ({
668
+ skillSlug: item.skill.slug,
669
+ skillName: item.skill.name,
670
+ artifactName: item.publishedArtifact.frontmatterName,
671
+ artifactDescription: item.publishedArtifact.frontmatterDescription,
672
+ whenToUse: item.skill.whenToUse ?? null,
673
+ version: item.skillVersion.version,
674
+ assignmentSource: 'CATALOG',
675
+ assignmentType: 'PATH',
676
+ scopePath: '',
677
+ includeChildren: true,
678
+ checksum: item.publishedArtifact.checksum,
679
+ publishedAt: item.publishedArtifact.publishedAt,
680
+ identifiers: getSkillIdentifiers({
681
+ ...item,
682
+ assignmentSource: 'CATALOG',
683
+ assignmentType: 'PATH',
684
+ scopePath: '',
685
+ includeChildren: true
686
+ }),
687
+ tags: item.skill.tags.map(toPublishedSkillTagSummary),
688
+ contextKind: 'installable',
689
+ installPolicy: item.skill.installPolicy,
690
+ policyLabel: getSkillPolicyLabel(item.skill.installPolicy, 'installable')
691
+ });
517
692
  export const toPublishedSkillCatalog = payload => ({
518
693
  pluginId: PLUGIN_ID,
519
694
  runtimeMode: 'tool_fetch_only',
520
- deliveryModel: 'backend_published_global_project_assignments',
695
+ deliveryModel: 'backend_published_installed_effective_skills',
521
696
  workspace: payload.workspace,
522
697
  directoryPath: payload.directoryPath,
523
698
  rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
@@ -527,6 +702,36 @@ export const toPublishedSkillCatalog = payload => ({
527
702
  facets: getPublishedSkillFacets(payload.skills),
528
703
  skills: payload.skills.map(toPublishedSkillSummary)
529
704
  });
705
+ const filterIgnoredPublishedSkills = async (config, result) => {
706
+ const authState = await readGlobalAuthState(config.authStatePath);
707
+ const userKey = toStoredUserKey(authState);
708
+ if (!result.fetchResult.ok) {
709
+ return {
710
+ ...result,
711
+ ignoreState: {
712
+ scopeKey: getPublishedSkillIgnoreScopeKey(result.workspaceResolution),
713
+ userKey,
714
+ ignoredSkillSlugs: [],
715
+ installedGlobalSkillSlugs: [],
716
+ installedWorkspaceSkillSlugs: []
717
+ },
718
+ ignoredSkills: []
719
+ };
720
+ }
721
+ const ignoredSkills = result.fetchResult.payload.userPreferences.ignoredSkills.map(toPublishedSkillSummary);
722
+ const ignoredSkillSlugs = ignoredSkills.map(skill => skill.skillSlug);
723
+ return {
724
+ ...result,
725
+ ignoreState: {
726
+ scopeKey: result.fetchResult.payload.userPreferences.scopeKey,
727
+ userKey: result.fetchResult.payload.userPreferences.userKey || userKey,
728
+ ignoredSkillSlugs,
729
+ installedGlobalSkillSlugs: [],
730
+ installedWorkspaceSkillSlugs: []
731
+ },
732
+ ignoredSkills
733
+ };
734
+ };
530
735
  const getWorkspaceUnavailableMessage = payload => {
531
736
  if (payload.workspace) return null;
532
737
  return 'Workspace-specific skills are unavailable because the workspace was not found; global skills are still loaded.';
@@ -601,7 +806,7 @@ const buildSkillDetailSnippetLine = detail => {
601
806
  const body = detail.markdownBody || detail.renderedContent || detail.markdownDocument;
602
807
  return `- ${detail.artifactName || detail.skillName}: ${truncateText(body, 700)}`;
603
808
  };
604
- const buildSystemNote = (result, config, details) => {
809
+ export const buildSystemNote = (result, config, details) => {
605
810
  if (!result.fetchResult.ok) return null;
606
811
  const catalog = toPublishedSkillCatalog(result.fetchResult.payload);
607
812
  const skillNames = catalog.skills.map(skill => skill.artifactName || skill.skillName || skill.skillSlug);
@@ -612,7 +817,7 @@ const buildSystemNote = (result, config, details) => {
612
817
  const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project').slice(0, 5).map(buildSkillCatalogLine);
613
818
  const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
614
819
  const detailBlock = detailLines.length > 0 ? ` Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
615
- 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(' ');
820
+ return [result.fetchResult.payload.workspace ? `Prefer opencode-wizard backend-published fetched skill bodies for scoped/private wizard skills in workspace ${result.fetchResult.payload.workspace.slug}.` : 'Prefer opencode-wizard backend-published global fetched skill bodies; 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.`, 'Use catalog whenToUse guidance to decide applicability; when it matches the task, fetch full bodies with opencode_wizard_published_skills_fetch and prefer those fetched bodies for current scoped/private wizard guidance.', '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, 'Local/native sources can still complement wizard skills: .opencode/skills is source seed content, skills.urls is a public/static complement, and backend-published fetched bodies are preferred for private/scoped wizard guidance.', `Root source seed path remains seed/source content: ${config.rootSkillSeedPath}/**.`].filter(line => line.length > 0).join(' ');
616
821
  };
617
822
  const toWorkspaceResolutionOutput = resolution => ({
618
823
  requestedDirectory: resolution.requestedDirectory,
@@ -629,6 +834,7 @@ const toWorkspaceResolutionMetadata = resolution => ({
629
834
  });
630
835
  const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
631
836
  const authState = await resolveStoredAuthState(worktree, config);
837
+ const filteredResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
632
838
  const base = {
633
839
  pluginId: PLUGIN_ID,
634
840
  runtimeMode: 'tool_fetch_only',
@@ -657,21 +863,26 @@ const formatStatusOutput = async (worktree, config, publishedSkillsResult, login
657
863
  email: loginBootstrapSnapshot.email,
658
864
  message: loginBootstrapSnapshot.message
659
865
  },
660
- status: publishedSkillsResult.fetchResult.status,
661
- fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
662
- source: publishedSkillsResult.fetchResult.source,
663
- availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS
866
+ status: filteredResult.fetchResult.status,
867
+ fetchedAt: filteredResult.fetchResult.fetchedAt,
868
+ source: filteredResult.fetchResult.source,
869
+ availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
870
+ ignoredPublishedSkills: {
871
+ scopeKey: filteredResult.ignoreState.scopeKey,
872
+ userKey: filteredResult.ignoreState.userKey,
873
+ count: filteredResult.ignoreState.ignoredSkillSlugs.length
874
+ }
664
875
  };
665
- if (!publishedSkillsResult.fetchResult.ok) {
876
+ if (!filteredResult.fetchResult.ok) {
666
877
  return JSON.stringify({
667
878
  ...base,
668
- message: publishedSkillsResult.fetchResult.message
879
+ message: filteredResult.fetchResult.message
669
880
  }, null, 2);
670
881
  }
671
882
  return JSON.stringify({
672
883
  ...base,
673
- ...toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload),
674
- message: getWorkspaceUnavailableMessage(publishedSkillsResult.fetchResult.payload)
884
+ ...toPublishedSkillCatalog(filteredResult.fetchResult.payload),
885
+ message: getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload)
675
886
  }, null, 2);
676
887
  };
677
888
  export const toPluginAuthStateSummary = authState => {
@@ -703,6 +914,11 @@ export const resolvePluginStatusSnapshot = async ({
703
914
  directory
704
915
  });
705
916
  const fetchResult = await fetchPublishedSkillsCatalog(worktree, config, workspaceResolution, signal);
917
+ const filteredResult = await filterIgnoredPublishedSkills(config, {
918
+ directoryPath: workspaceResolution.directoryPath,
919
+ workspaceResolution,
920
+ fetchResult
921
+ });
706
922
  const authState = await resolveStoredAuthState(worktree, config);
707
923
  return {
708
924
  pluginId: PLUGIN_ID,
@@ -715,19 +931,43 @@ export const resolvePluginStatusSnapshot = async ({
715
931
  rootSkillSeedPath: config.rootSkillSeedPath,
716
932
  authStatePath: config.authStatePath,
717
933
  authState: toPluginAuthStateSummary(authState),
718
- status: fetchResult.status,
719
- authMode: fetchResult.authMode,
720
- fetchedAt: fetchResult.fetchedAt,
721
- source: fetchResult.source,
934
+ status: filteredResult.fetchResult.status,
935
+ authMode: filteredResult.fetchResult.authMode,
936
+ fetchedAt: filteredResult.fetchResult.fetchedAt,
937
+ source: filteredResult.fetchResult.source,
722
938
  availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
723
- message: fetchResult.ok ? getWorkspaceUnavailableMessage(fetchResult.payload) : fetchResult.message,
724
- catalog: fetchResult.ok ? toPublishedSkillCatalog(fetchResult.payload) : null
939
+ message: filteredResult.fetchResult.ok ? getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload) : filteredResult.fetchResult.message,
940
+ catalog: filteredResult.fetchResult.ok ? toPublishedSkillCatalog(filteredResult.fetchResult.payload) : null,
941
+ installableCatalog: filteredResult.fetchResult.ok ? {
942
+ count: filteredResult.fetchResult.payload.catalogSkills.length,
943
+ skills: filteredResult.fetchResult.payload.catalogSkills.map(toInstallableSkillSummary)
944
+ } : null,
945
+ ignoredPublishedSkills: {
946
+ scopeKey: filteredResult.ignoreState.scopeKey,
947
+ userKey: filteredResult.ignoreState.userKey,
948
+ count: filteredResult.ignoreState.ignoredSkillSlugs.length,
949
+ skills: filteredResult.ignoredSkills
950
+ }
725
951
  };
726
952
  };
727
953
  const withStatusMessage = (snapshot, message) => ({
728
954
  ...snapshot,
729
955
  message
730
956
  });
957
+ const toAiFacingPluginStatusSnapshot = snapshot => {
958
+ const {
959
+ ignoredPublishedSkills,
960
+ installableCatalog: _installableCatalog,
961
+ ...safeSnapshot
962
+ } = snapshot;
963
+ return {
964
+ ...safeSnapshot,
965
+ ignoredPublishedSkills: {
966
+ scopeKey: ignoredPublishedSkills.scopeKey,
967
+ count: ignoredPublishedSkills.count
968
+ }
969
+ };
970
+ };
731
971
  const startStatusPathLoginBootstrap = (worktree, config) => {
732
972
  if (statusPathLoginBootstrap.promise) return;
733
973
  if (statusPathLoginBootstrap.status === 'failed' && statusPathLoginBootstrap.failedAt && Date.now() - statusPathLoginBootstrap.failedAt < STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS) {
@@ -794,6 +1034,93 @@ export const resolvePluginStatusSnapshotWithAuthBootstrap = async ({
794
1034
  }
795
1035
  return withStatusMessage(snapshot, 'Browser login is pending from the TUI/status path.');
796
1036
  };
1037
+ const toBackendPreferenceScope = preferenceScope => {
1038
+ if (preferenceScope === 'global') return 'GLOBAL';
1039
+ return 'WORKSPACE';
1040
+ };
1041
+ const setPublishedSkillPreference = async ({
1042
+ worktree,
1043
+ directory,
1044
+ config,
1045
+ skillSlug,
1046
+ preferenceScope,
1047
+ installed,
1048
+ ignored
1049
+ }) => {
1050
+ const workspaceResolution = await resolveWorkspace({
1051
+ config,
1052
+ directory
1053
+ });
1054
+ const response = await fetchPublishedSkillsGraphQl({
1055
+ worktree,
1056
+ config,
1057
+ query: SET_PUBLISHED_SKILL_PREFERENCE_MUTATION,
1058
+ variables: {
1059
+ input: {
1060
+ ...toDeliveryInput(workspaceResolution),
1061
+ skillSlug,
1062
+ preferenceScope: toBackendPreferenceScope(preferenceScope),
1063
+ installed,
1064
+ ignored
1065
+ }
1066
+ },
1067
+ signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
1068
+ });
1069
+ if (!response.ok) {
1070
+ throw new Error(response.result.message);
1071
+ }
1072
+ const preferences = response.data.setPublishedSkillPreference;
1073
+ publishedSkillPreferenceCacheVersion += 1;
1074
+ return {
1075
+ scopeKey: preferences.scopeKey,
1076
+ userKey: preferences.userKey,
1077
+ ignoredSkillSlugs: preferences.ignoredSkills.map(item => item.skill.slug),
1078
+ installedGlobalSkillSlugs: [],
1079
+ installedWorkspaceSkillSlugs: []
1080
+ };
1081
+ };
1082
+ export const setPublishedSkillIgnored = async ({
1083
+ worktree,
1084
+ directory,
1085
+ skillSlug,
1086
+ ignored,
1087
+ preferenceScope
1088
+ }) => {
1089
+ const config = await resolveConfig(worktree);
1090
+ const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
1091
+ if (!normalizedSkillSlug) {
1092
+ throw new Error('Cannot toggle an empty published skill slug.');
1093
+ }
1094
+ return setPublishedSkillPreference({
1095
+ worktree,
1096
+ directory,
1097
+ config,
1098
+ skillSlug: normalizedSkillSlug,
1099
+ preferenceScope: preferenceScope ?? 'project',
1100
+ ignored
1101
+ });
1102
+ };
1103
+ export const setPublishedSkillInstalled = async ({
1104
+ worktree,
1105
+ directory,
1106
+ skillSlug,
1107
+ installed,
1108
+ preferenceScope
1109
+ }) => {
1110
+ const config = await resolveConfig(worktree);
1111
+ const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
1112
+ if (!normalizedSkillSlug) {
1113
+ throw new Error('Cannot toggle an empty published skill slug.');
1114
+ }
1115
+ return setPublishedSkillPreference({
1116
+ worktree,
1117
+ directory,
1118
+ config,
1119
+ skillSlug: normalizedSkillSlug,
1120
+ preferenceScope,
1121
+ installed
1122
+ });
1123
+ };
797
1124
  const toPluginStatusMetadata = snapshot => ({
798
1125
  backendOrigin: snapshot.backendOrigin,
799
1126
  graphqlUrl: snapshot.graphqlUrl,
@@ -1503,11 +1830,11 @@ const openBrowser = async url => {
1503
1830
  const normalizeDirectoryArg = (contextDirectory, directory) => {
1504
1831
  return normalizeAbsolutePath(directory ? path.resolve(contextDirectory, directory) : contextDirectory);
1505
1832
  };
1506
- const getDetailCacheKey = (workspaceResolution, skillVersionId) => {
1507
- return JSON.stringify([workspaceResolution.cacheKey, skillVersionId]);
1833
+ const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
1834
+ return JSON.stringify([catalogCacheKey, skillVersionId]);
1508
1835
  };
1509
- const getDetailInflightKey = (workspaceResolution, skillVersionId, purpose) => {
1510
- return JSON.stringify([workspaceResolution.cacheKey, skillVersionId, purpose]);
1836
+ const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
1837
+ return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
1511
1838
  };
1512
1839
  const OpencodeWizardSkillsPlugin = async input => {
1513
1840
  const {
@@ -1727,7 +2054,8 @@ const OpencodeWizardSkillsPlugin = async input => {
1727
2054
  directory
1728
2055
  });
1729
2056
  const directoryPath = workspaceResolution.directoryPath;
1730
- const cacheKey = workspaceResolution.cacheKey;
2057
+ const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
2058
+ const cacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
1731
2059
  const cached = cache.get(cacheKey);
1732
2060
  if (useCache && cached && cached.expiresAt > Date.now()) {
1733
2061
  return {
@@ -1770,8 +2098,10 @@ const OpencodeWizardSkillsPlugin = async input => {
1770
2098
  purpose
1771
2099
  }) => {
1772
2100
  const directoryPath = workspaceResolution.directoryPath;
1773
- const cacheKey = getDetailCacheKey(workspaceResolution, item.skillVersion.id);
1774
- const inflightKey = getDetailInflightKey(workspaceResolution, item.skillVersion.id, purpose);
2101
+ const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
2102
+ const catalogCacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
2103
+ const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
2104
+ const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
1775
2105
  const cached = detailCache.get(cacheKey);
1776
2106
  if (purpose === 'SYSTEM_CONTEXT' && useCache && cached && cached.expiresAt > Date.now()) {
1777
2107
  return {
@@ -1911,18 +2241,29 @@ const OpencodeWizardSkillsPlugin = async input => {
1911
2241
  loginBootstrapSnapshot: loginBootstrap.snapshot
1912
2242
  });
1913
2243
  }
1914
- const selection = selectPublishedSkills(publishedSkillsResult.fetchResult.payload, requestedSkills);
2244
+ const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
2245
+ if (!filteredPublishedSkillsResult.fetchResult.ok) {
2246
+ await emitFetchOutcome('FETCH_FAILED');
2247
+ return toFetchFailureOutput({
2248
+ worktree: input.worktree,
2249
+ config,
2250
+ publishedSkillsResult: filteredPublishedSkillsResult,
2251
+ loginBootstrapSnapshot: loginBootstrap.snapshot
2252
+ });
2253
+ }
2254
+ const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
1915
2255
  const isSingleRequest = requestedSkills.length === 1;
1916
2256
  if (requestedSkills.length === 0) {
1917
- const catalog = toPublishedSkillCatalog(publishedSkillsResult.fetchResult.payload);
2257
+ const catalog = toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload);
1918
2258
  context.metadata({
1919
- title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount}`,
2259
+ title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount} active`,
1920
2260
  metadata: {
1921
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2261
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
1922
2262
  status: 'ready',
1923
2263
  publishedSkillCount: catalog.publishedSkillCount.toString(),
1924
2264
  globalAssignmentCount: catalog.assignmentCounts.global.toString(),
1925
- projectAssignmentCount: catalog.assignmentCounts.project.toString()
2265
+ projectAssignmentCount: catalog.assignmentCounts.project.toString(),
2266
+ ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
1926
2267
  }
1927
2268
  });
1928
2269
  await emitFetchOutcome('FETCH_SUCCESS');
@@ -1930,16 +2271,21 @@ const OpencodeWizardSkillsPlugin = async input => {
1930
2271
  output: JSON.stringify({
1931
2272
  ...catalog,
1932
2273
  status: 'ready',
1933
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
1934
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
1935
- fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
1936
- source: publishedSkillsResult.fetchResult.source,
2274
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2275
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2276
+ ignoredPublishedSkills: {
2277
+ scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
2278
+ count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
2279
+ },
2280
+ fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2281
+ source: filteredPublishedSkillsResult.fetchResult.source,
1937
2282
  message: 'Catalog discovery only. Provide `skill` or `skills` to fetch markdown bodies/details for selected skills.'
1938
2283
  }, null, 2),
1939
2284
  metadata: {
1940
2285
  status: 'ready',
1941
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
1942
- publishedSkillCount: catalog.publishedSkillCount.toString()
2286
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2287
+ publishedSkillCount: catalog.publishedSkillCount.toString(),
2288
+ ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
1943
2289
  }
1944
2290
  };
1945
2291
  }
@@ -1950,19 +2296,23 @@ const OpencodeWizardSkillsPlugin = async input => {
1950
2296
  pluginId: PLUGIN_ID,
1951
2297
  runtimeMode: 'tool_fetch_only',
1952
2298
  status: 'not_found',
1953
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
1954
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
2299
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2300
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
1955
2301
  requestedSkill: requestedSkills[0],
1956
- availableSkills: publishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary)
2302
+ availableSkills: filteredPublishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary),
2303
+ ignoredPublishedSkills: {
2304
+ scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
2305
+ count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
2306
+ }
1957
2307
  }, null, 2),
1958
2308
  metadata: {
1959
2309
  status: 'not_found',
1960
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution)
2310
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution)
1961
2311
  }
1962
2312
  };
1963
2313
  }
1964
2314
  let skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
1965
- workspaceResolution: publishedSkillsResult.workspaceResolution,
2315
+ workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
1966
2316
  item,
1967
2317
  signal: context.abort,
1968
2318
  useCache: !args.refresh,
@@ -1974,7 +2324,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1974
2324
  await schedulePresenceStart(authState);
1975
2325
  });
1976
2326
  skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
1977
- workspaceResolution: publishedSkillsResult.workspaceResolution,
2327
+ workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
1978
2328
  item,
1979
2329
  signal: context.abort,
1980
2330
  useCache: false,
@@ -2000,7 +2350,7 @@ const OpencodeWizardSkillsPlugin = async input => {
2000
2350
  context.metadata({
2001
2351
  title: `opencode-wizard published skill: ${detail.artifactName || detail.skillName}`,
2002
2352
  metadata: {
2003
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2353
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2004
2354
  skillSlug: detail.skillSlug,
2005
2355
  version: detail.version
2006
2356
  }
@@ -2010,16 +2360,16 @@ const OpencodeWizardSkillsPlugin = async input => {
2010
2360
  output: JSON.stringify({
2011
2361
  pluginId: PLUGIN_ID,
2012
2362
  runtimeMode: 'tool_fetch_only',
2013
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
2014
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
2015
- workspace: publishedSkillsResult.fetchResult.payload.workspace,
2016
- fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
2017
- source: publishedSkillsResult.fetchResult.source,
2363
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2364
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2365
+ workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
2366
+ fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2367
+ source: filteredPublishedSkillsResult.fetchResult.source,
2018
2368
  skill: detail
2019
2369
  }, null, 2),
2020
2370
  metadata: {
2021
2371
  status: 'ready',
2022
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2372
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2023
2373
  skillSlug: detail.skillSlug
2024
2374
  }
2025
2375
  };
@@ -2027,7 +2377,7 @@ const OpencodeWizardSkillsPlugin = async input => {
2027
2377
  context.metadata({
2028
2378
  title: `opencode-wizard published skills fetch: ${skillDetails.length}`,
2029
2379
  metadata: {
2030
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2380
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2031
2381
  requestedCount: requestedSkills.length.toString(),
2032
2382
  matchedCount: skillDetails.length.toString()
2033
2383
  }
@@ -2037,18 +2387,18 @@ const OpencodeWizardSkillsPlugin = async input => {
2037
2387
  output: JSON.stringify({
2038
2388
  pluginId: PLUGIN_ID,
2039
2389
  runtimeMode: 'tool_fetch_only',
2040
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
2041
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
2042
- workspace: publishedSkillsResult.fetchResult.payload.workspace,
2043
- fetchedAt: publishedSkillsResult.fetchResult.fetchedAt,
2044
- 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,
2045
2395
  requestedSkills,
2046
2396
  missingSkills: selection.missingIdentifiers,
2047
2397
  skills: skillDetails
2048
2398
  }, null, 2),
2049
2399
  metadata: {
2050
2400
  status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
2051
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
2401
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2052
2402
  matchedCount: skillDetails.length.toString()
2053
2403
  }
2054
2404
  };
@@ -2083,7 +2433,7 @@ const OpencodeWizardSkillsPlugin = async input => {
2083
2433
  metadata
2084
2434
  });
2085
2435
  return {
2086
- output: JSON.stringify(snapshot, null, 2),
2436
+ output: JSON.stringify(toAiFacingPluginStatusSnapshot(snapshot), null, 2),
2087
2437
  metadata
2088
2438
  };
2089
2439
  };
@@ -2143,11 +2493,12 @@ const OpencodeWizardSkillsPlugin = async input => {
2143
2493
  return;
2144
2494
  }
2145
2495
  }
2496
+ const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
2146
2497
  const details = await loadSystemNoteDetails({
2147
- publishedSkillsResult,
2498
+ publishedSkillsResult: filteredPublishedSkillsResult,
2148
2499
  signal: AbortSignal.timeout(5_000)
2149
2500
  });
2150
- const systemNote = buildSystemNote(publishedSkillsResult, config, details);
2501
+ const systemNote = buildSystemNote(filteredPublishedSkillsResult, config, details);
2151
2502
  if (!systemNote) return;
2152
2503
  output.system.push(systemNote);
2153
2504
  }