@aexol/opencode-wizard 0.1.15 → 0.2.0

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/README.md CHANGED
@@ -21,6 +21,7 @@ Useful commands:
21
21
 
22
22
  ```bash
23
23
  npm run typecheck
24
+ npm run test
24
25
  npm run build
25
26
  npm run release:check
26
27
  ```
@@ -45,7 +46,7 @@ Use `skills.urls` only for public registries that are intentionally cacheable by
45
46
 
46
47
  ## Catalog discovery and auth bootstrap
47
48
 
48
- On chat/system-context startup, the plugin attempts to load the catalog automatically with the stored plugin auth at `~/.config/opencode/opencode-wizard.json` (`auth` field). If no valid plugin session exists, startup stays passive and reports that interactive fetch will bootstrap browser login when needed; it does not open the browser from system context.
49
+ Catalog discovery uses the backend-issued plugin session token stored at `~/.config/opencode/opencode-wizard.json` (`auth` field); the plugin does not persist or send Microsoft/Entra tokens to GraphQL. If no valid plugin session exists, no-arg `opencode_wizard_published_skills_fetch`, explicit `opencode_wizard_status`, TUI status, and chat/system-context startup may start the browser Entra PKCE flow and exchange the callback for a fresh backend-issued plugin session.
49
50
 
50
51
  Call `opencode_wizard_published_skills_fetch` without `skill` or `skills` to manually bootstrap plugin login if needed and return catalog-only discovery output for the current directory scope.
51
52
 
@@ -0,0 +1,6 @@
1
+ export declare const PUBLISHED_BACKEND_ORIGIN = "https://opencode-wizard.aexol.work";
2
+ export declare const normalizeBackendOrigin: (value: string | undefined) => string | null;
3
+ export declare const resolveBackendOriginFromValues: ({ environmentBackendOrigin, localBackendOrigin, }: {
4
+ environmentBackendOrigin?: string;
5
+ localBackendOrigin?: string;
6
+ }) => string;
package/dist/config.js ADDED
@@ -0,0 +1,19 @@
1
+ export const PUBLISHED_BACKEND_ORIGIN = 'https://opencode-wizard.aexol.work';
2
+ export const normalizeBackendOrigin = value => {
3
+ if (!value) return null;
4
+ try {
5
+ const normalizedUrl = new URL(value);
6
+ return normalizedUrl.origin;
7
+ } catch {
8
+ return null;
9
+ }
10
+ };
11
+ export const resolveBackendOriginFromValues = ({
12
+ environmentBackendOrigin,
13
+ localBackendOrigin
14
+ }) => {
15
+ const configuredBackendOrigin = normalizeBackendOrigin(environmentBackendOrigin) ?? normalizeBackendOrigin(localBackendOrigin);
16
+ if (configuredBackendOrigin) return configuredBackendOrigin;
17
+ return PUBLISHED_BACKEND_ORIGIN;
18
+ };
19
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["PUBLISHED_BACKEND_ORIGIN","normalizeBackendOrigin","value","normalizedUrl","URL","origin","resolveBackendOriginFromValues","environmentBackendOrigin","localBackendOrigin","configuredBackendOrigin"],"sources":["../src/config.ts"],"sourcesContent":["export const PUBLISHED_BACKEND_ORIGIN = 'https://opencode-wizard.aexol.work';\n\nexport const normalizeBackendOrigin = (value: string | undefined): string | null => {\n if (!value) return null;\n\n try {\n const normalizedUrl = new URL(value);\n return normalizedUrl.origin;\n } catch {\n return null;\n }\n};\n\nexport const resolveBackendOriginFromValues = ({\n environmentBackendOrigin,\n localBackendOrigin,\n}: {\n environmentBackendOrigin?: string;\n localBackendOrigin?: string;\n}): string => {\n const configuredBackendOrigin =\n normalizeBackendOrigin(environmentBackendOrigin) ?? normalizeBackendOrigin(localBackendOrigin);\n\n if (configuredBackendOrigin) return configuredBackendOrigin;\n\n return PUBLISHED_BACKEND_ORIGIN;\n};\n"],"mappings":"AAAA,OAAO,MAAMA,wBAAwB,GAAG,oCAAoC;AAE5E,OAAO,MAAMC,sBAAsB,GAAIC,KAAyB,IAAoB;EAClF,IAAI,CAACA,KAAK,EAAE,OAAO,IAAI;EAEvB,IAAI;IACF,MAAMC,aAAa,GAAG,IAAIC,GAAG,CAACF,KAAK,CAAC;IACpC,OAAOC,aAAa,CAACE,MAAM;EAC7B,CAAC,CAAC,MAAM;IACN,OAAO,IAAI;EACb;AACF,CAAC;AAED,OAAO,MAAMC,8BAA8B,GAAGA,CAAC;EAC7CC,wBAAwB;EACxBC;AAIF,CAAC,KAAa;EACZ,MAAMC,uBAAuB,GAC3BR,sBAAsB,CAACM,wBAAwB,CAAC,IAAIN,sBAAsB,CAACO,kBAAkB,CAAC;EAEhG,IAAIC,uBAAuB,EAAE,OAAOA,uBAAuB;EAE3D,OAAOT,wBAAwB;AACjC,CAAC","ignoreList":[]}
package/dist/server.d.ts CHANGED
@@ -65,8 +65,18 @@ export type PublishedSkillCatalogItem = {
65
65
  frontmatterDescription: string;
66
66
  checksum: string;
67
67
  publishedAt: string;
68
+ fileCount: number;
68
69
  };
69
70
  };
71
+ type PublishedSkillArtifactFilePayload = {
72
+ id: string;
73
+ relativePath: string;
74
+ contentType: string;
75
+ content: string;
76
+ checksum: string;
77
+ size: number;
78
+ sortOrder: number;
79
+ };
70
80
  type PublishedSkillFacet = {
71
81
  id: string;
72
82
  slug: string;
@@ -96,6 +106,7 @@ export type PublishedSkillDetailItem = PublishedSkillCatalogItem & {
96
106
  publishedArtifact: PublishedSkillCatalogItem['publishedArtifact'] & {
97
107
  markdownBody: string;
98
108
  renderedContent: string;
109
+ files: PublishedSkillArtifactFilePayload[];
99
110
  };
100
111
  };
101
112
  type AuthState = {
@@ -134,10 +145,12 @@ type PublishedSkillSummary = {
134
145
  includeChildren: boolean | null;
135
146
  checksum: string;
136
147
  publishedAt: string;
148
+ fileCount: number;
137
149
  identifiers: string[];
138
150
  tags: PublishedSkillTagSummary[];
139
151
  contextKind: 'global' | 'project' | 'installable';
140
152
  installPolicy: PublishedSkillInstallPolicy;
153
+ assignmentLabel: string;
141
154
  policyLabel: string;
142
155
  };
143
156
  type PublishedSkillDetail = PublishedSkillSummary & {
@@ -147,6 +160,8 @@ type PublishedSkillDetail = PublishedSkillSummary & {
147
160
  markdownDocument: string;
148
161
  markdownBody: string;
149
162
  renderedContent: string;
163
+ files: PublishedSkillArtifactFilePayload[];
164
+ resources: PublishedSkillArtifactFilePayload[];
150
165
  };
151
166
  type OpencodePluginServerInput = {
152
167
  worktree: string;
@@ -171,6 +186,7 @@ type PublishedSkillsSuccessState = {
171
186
  assignmentCounts: {
172
187
  global: number;
173
188
  project: number;
189
+ user: number;
174
190
  other: number;
175
191
  };
176
192
  facets: PublishedSkillFacetSummary[];
@@ -185,6 +201,11 @@ export type NativeSkillsUrlCompatibility = {
185
201
  guidance: string;
186
202
  };
187
203
  export declare const NATIVE_SKILLS_URL_COMPATIBILITY: NativeSkillsUrlCompatibility;
204
+ type PublishedSkillsResult = {
205
+ directoryPath: string;
206
+ workspaceResolution: WorkspaceResolution;
207
+ fetchResult: FetchResult;
208
+ };
188
209
  type PublishedSkillsIgnoreState = {
189
210
  scopeKey: string;
190
211
  userKey: string;
@@ -238,6 +259,7 @@ export declare const selectPublishedSkills: <TItem extends PublishedSkillCatalog
238
259
  selectedItems: TItem[];
239
260
  missingIdentifiers: string[];
240
261
  };
262
+ export declare const buildSystemNote: (result: PublishedSkillsResult, config: ResolvedConfig, details: PublishedSkillDetail[]) => string | null;
241
263
  declare const toWorkspaceResolutionOutput: (resolution: WorkspaceResolution) => {
242
264
  requestedDirectory: string;
243
265
  repositoryRoot: string;
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';
@@ -113,6 +114,7 @@ const PUBLISHED_SKILLS_CATALOG_QUERY = `
113
114
  frontmatterDescription
114
115
  checksum
115
116
  publishedAt
117
+ fileCount
116
118
  }
117
119
  }
118
120
  catalogSkills {
@@ -150,6 +152,7 @@ const PUBLISHED_SKILLS_CATALOG_QUERY = `
150
152
  frontmatterDescription
151
153
  checksum
152
154
  publishedAt
155
+ fileCount
153
156
  }
154
157
  }
155
158
  userPreferences {
@@ -194,6 +197,7 @@ const PUBLISHED_SKILLS_CATALOG_QUERY = `
194
197
  frontmatterDescription
195
198
  checksum
196
199
  publishedAt
200
+ fileCount
197
201
  }
198
202
  }
199
203
  }
@@ -244,6 +248,7 @@ const SET_PUBLISHED_SKILL_PREFERENCE_MUTATION = `
244
248
  frontmatterDescription
245
249
  checksum
246
250
  publishedAt
251
+ fileCount
247
252
  }
248
253
  }
249
254
  }
@@ -259,6 +264,16 @@ const PUBLISHED_SKILL_DETAIL_QUERY = `
259
264
  renderedContent
260
265
  checksum
261
266
  publishedAt
267
+ fileCount
268
+ files {
269
+ id
270
+ relativePath
271
+ contentType
272
+ content
273
+ checksum
274
+ size
275
+ sortOrder
276
+ }
262
277
  }
263
278
  }
264
279
  `;
@@ -307,20 +322,12 @@ const readLocalEnvValues = async startDirectory => {
307
322
  return new Map();
308
323
  }
309
324
  };
310
- const normalizeBackendOrigin = value => {
311
- if (!value) return null;
312
- try {
313
- const normalizedUrl = new URL(value);
314
- return normalizedUrl.origin;
315
- } catch {
316
- return null;
317
- }
318
- };
319
325
  const resolveBackendOrigin = async worktree => {
320
326
  const envValues = await readLocalEnvValues(worktree);
321
- const configuredBackendOrigin = normalizeBackendOrigin(process.env.OPENCODE_WIZARD_BACKEND_ORIGIN) ?? normalizeBackendOrigin(envValues.get('OPENCODE_WIZARD_BACKEND_ORIGIN'));
322
- if (configuredBackendOrigin) return configuredBackendOrigin;
323
- return PUBLISHED_BACKEND_ORIGIN;
327
+ return resolveBackendOriginFromValues({
328
+ environmentBackendOrigin: process.env.OPENCODE_WIZARD_BACKEND_ORIGIN,
329
+ localBackendOrigin: envValues.get('OPENCODE_WIZARD_BACKEND_ORIGIN')
330
+ });
324
331
  };
325
332
  const toWorkspaceSlug = value => {
326
333
  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '');
@@ -436,26 +443,6 @@ const formatSkillLabel = item => {
436
443
  return item.skill.name;
437
444
  };
438
445
  const toFrontmatterString = value => JSON.stringify(value);
439
- const readJsonFile = async filePath => {
440
- try {
441
- const raw = await fs.readFile(filePath, 'utf8');
442
- const parsed = JSON.parse(raw);
443
- return parsed;
444
- } catch {
445
- return null;
446
- }
447
- };
448
- const writeJsonFile = async (filePath, value) => {
449
- await fs.mkdir(path.dirname(filePath), {
450
- recursive: true
451
- });
452
- await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
453
- };
454
- const deleteFileIfExists = async filePath => {
455
- await fs.rm(filePath, {
456
- force: true
457
- });
458
- };
459
446
  const isRecord = value => {
460
447
  return typeof value === 'object' && value !== null && !Array.isArray(value);
461
448
  };
@@ -472,7 +459,7 @@ const readGlobalConfig = async configFile => {
472
459
  return {};
473
460
  };
474
461
  const writeGlobalConfig = async (configFile, config) => {
475
- await writeJsonFile(configFile, config);
462
+ await writePrivateJsonFile(configFile, config);
476
463
  };
477
464
  const withoutLegacyPublishedSkillPreferences = config => {
478
465
  const {
@@ -628,7 +615,22 @@ const getPublishedSkillFacets = items => {
628
615
  }
629
616
  return [...facetsBySlug.values()].sort((left, right) => left.slug.localeCompare(right.slug));
630
617
  };
618
+ const isUserPublishedSkillAssignment = assignmentSource => assignmentSource === 'USER' || assignmentSource === 'USER_GLOBAL' || assignmentSource === 'USER_WORKSPACE';
619
+ const getPublishedSkillAssignmentLabel = assignmentSource => {
620
+ if (assignmentSource === 'GLOBAL') return 'GLOBAL SCOPE assignment';
621
+ if (assignmentSource === 'WORKSPACE') return 'PROJECT SCOPE assignment';
622
+ if (assignmentSource === 'USER_GLOBAL') return 'USER SCOPE preference (global target)';
623
+ if (assignmentSource === 'USER_WORKSPACE') return 'USER SCOPE preference (project target)';
624
+ if (assignmentSource === 'USER') return 'USER SCOPE assignment';
625
+ return `${assignmentSource.toUpperCase().replace(/_/gu, ' ')} assignment`;
626
+ };
631
627
  const getPublishedSkillAssignmentCounts = items => items.reduce((counts, item) => {
628
+ if (isUserPublishedSkillAssignment(item.assignmentSource)) {
629
+ return {
630
+ ...counts,
631
+ user: counts.user + 1
632
+ };
633
+ }
632
634
  if (item.assignmentSource === 'GLOBAL') {
633
635
  return {
634
636
  ...counts,
@@ -648,17 +650,22 @@ const getPublishedSkillAssignmentCounts = items => items.reduce((counts, item) =
648
650
  }, {
649
651
  global: 0,
650
652
  project: 0,
653
+ user: 0,
651
654
  other: 0
652
655
  });
653
656
  const getSkillContextKind = item => {
654
657
  if (item.assignmentSource === 'GLOBAL' || item.assignmentSource === 'USER_GLOBAL') return 'global';
655
658
  return 'project';
656
659
  };
657
- const getSkillPolicyLabel = (policy, contextKind) => {
660
+ const getSkillPolicyLabel = (policy, contextKind, assignmentSource) => {
661
+ if (isUserPublishedSkillAssignment(assignmentSource) && policy === 'GLOBAL_CONTEXT') {
662
+ return 'GLOBAL_CONTEXT · active USER SCOPE context';
663
+ }
664
+ if (isUserPublishedSkillAssignment(assignmentSource)) return 'PROJECT_INSTALLABLE · active USER SCOPE preference';
658
665
  if (policy === 'GLOBAL_CONTEXT') return 'GLOBAL_CONTEXT · active context only, not project-installable';
659
666
  if (contextKind === 'installable') return 'PROJECT_INSTALLABLE · available to install';
660
- if (contextKind === 'global') return 'PROJECT_INSTALLABLE · active global assignment';
661
- return 'PROJECT_INSTALLABLE · active project/workspace assignment';
667
+ if (contextKind === 'global') return 'PROJECT_INSTALLABLE · active GLOBAL SCOPE assignment';
668
+ return 'PROJECT_INSTALLABLE · active PROJECT SCOPE assignment';
662
669
  };
663
670
  const toPublishedSkillSummary = item => {
664
671
  const contextKind = getSkillContextKind(item);
@@ -675,11 +682,13 @@ const toPublishedSkillSummary = item => {
675
682
  includeChildren: item.includeChildren ?? null,
676
683
  checksum: item.publishedArtifact.checksum,
677
684
  publishedAt: item.publishedArtifact.publishedAt,
685
+ fileCount: item.publishedArtifact.fileCount,
678
686
  identifiers: getSkillIdentifiers(item),
679
687
  tags: item.skill.tags.map(toPublishedSkillTagSummary),
680
688
  contextKind,
681
689
  installPolicy: item.skill.installPolicy,
682
- policyLabel: getSkillPolicyLabel(item.skill.installPolicy, contextKind)
690
+ assignmentLabel: getPublishedSkillAssignmentLabel(item.assignmentSource),
691
+ policyLabel: getSkillPolicyLabel(item.skill.installPolicy, contextKind, item.assignmentSource)
683
692
  };
684
693
  };
685
694
  export const toPublishedSkillDetail = item => ({
@@ -689,7 +698,9 @@ export const toPublishedSkillDetail = item => ({
689
698
  artifactId: item.publishedArtifact.id,
690
699
  markdownDocument: buildSkillMarkdown(item),
691
700
  markdownBody: item.publishedArtifact.markdownBody,
692
- renderedContent: item.publishedArtifact.renderedContent
701
+ renderedContent: item.publishedArtifact.renderedContent,
702
+ files: item.publishedArtifact.files,
703
+ resources: item.publishedArtifact.files.filter(file => file.relativePath !== 'SKILL.md')
693
704
  });
694
705
  const toInstallableSkillSummary = item => ({
695
706
  skillSlug: item.skill.slug,
@@ -704,6 +715,7 @@ const toInstallableSkillSummary = item => ({
704
715
  includeChildren: true,
705
716
  checksum: item.publishedArtifact.checksum,
706
717
  publishedAt: item.publishedArtifact.publishedAt,
718
+ fileCount: item.publishedArtifact.fileCount,
707
719
  identifiers: getSkillIdentifiers({
708
720
  ...item,
709
721
  assignmentSource: 'CATALOG',
@@ -714,7 +726,8 @@ const toInstallableSkillSummary = item => ({
714
726
  tags: item.skill.tags.map(toPublishedSkillTagSummary),
715
727
  contextKind: 'installable',
716
728
  installPolicy: item.skill.installPolicy,
717
- policyLabel: getSkillPolicyLabel(item.skill.installPolicy, 'installable')
729
+ assignmentLabel: 'catalog skill',
730
+ policyLabel: getSkillPolicyLabel(item.skill.installPolicy, 'installable', 'CATALOG')
718
731
  });
719
732
  export const toPublishedSkillCatalog = payload => ({
720
733
  pluginId: PLUGIN_ID,
@@ -827,24 +840,27 @@ const truncateText = (value, maxLength) => {
827
840
  };
828
841
  const buildSkillCatalogLine = skill => {
829
842
  const description = truncateText(skill.whenToUse || skill.artifactDescription || skill.skillName || skill.skillSlug, SYSTEM_NOTE_SKILL_DESCRIPTION_LIMIT);
830
- return `- ${skill.artifactName || skill.skillName} (${skill.skillSlug}, ${skill.contextKind.toUpperCase()}): ${description}`;
843
+ const scopeLabel = isUserPublishedSkillAssignment(skill.assignmentSource) ? 'USER SCOPE' : skill.contextKind === 'global' ? 'GLOBAL SCOPE' : 'PROJECT SCOPE';
844
+ const assignmentLabel = skill.assignmentSource.toLowerCase().replace(/_/gu, ' ');
845
+ return `- ${skill.artifactName || skill.skillName} (${skill.skillSlug}, ${assignmentLabel} assignment) [${scopeLabel}]: ${description}`;
831
846
  };
832
847
  const buildSkillDetailSnippetLine = detail => {
833
848
  const body = detail.markdownBody || detail.renderedContent || detail.markdownDocument;
834
849
  return `- ${detail.artifactName || detail.skillName}: ${truncateText(body, 700)}`;
835
850
  };
836
- const buildSystemNote = (result, config, details) => {
851
+ export const buildSystemNote = (result, config, details) => {
837
852
  if (!result.fetchResult.ok) return null;
838
853
  const catalog = toPublishedSkillCatalog(result.fetchResult.payload);
839
854
  const skillNames = catalog.skills.map(skill => skill.artifactName || skill.skillName || skill.skillSlug);
840
855
  const renderedSkillNames = skillNames.length > 0 ? skillNames.slice(0, SYSTEM_NOTE_SKILL_NAME_LIMIT).join(', ') : 'none';
841
856
  const remainingCount = Math.max(skillNames.length - SYSTEM_NOTE_SKILL_NAME_LIMIT, 0);
842
857
  const renderedCountSuffix = remainingCount > 0 ? ` (+${remainingCount} more)` : '';
843
- const globalSkills = catalog.skills.filter(skill => skill.contextKind === 'global').slice(0, 8).map(buildSkillCatalogLine);
844
- const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project').slice(0, 5).map(buildSkillCatalogLine);
858
+ const globalSkills = catalog.skills.filter(skill => skill.contextKind === 'global' && !isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 8).map(buildSkillCatalogLine);
859
+ const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project' && !isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 5).map(buildSkillCatalogLine);
860
+ const userSkills = catalog.skills.filter(skill => isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 5).map(buildSkillCatalogLine);
845
861
  const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
846
862
  const detailBlock = detailLines.length > 0 ? ` Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
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(' ');
863
+ 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} workspace, ${catalog.assignmentCounts.user} user, ${catalog.assignmentCounts.other} other (GLOBAL SCOPE: ${catalog.assignmentCounts.global}, PROJECT SCOPE: ${catalog.assignmentCounts.project}, USER SCOPE: ${catalog.assignmentCounts.user}).`, '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 to GLOBAL SCOPE, PROJECT SCOPE, or USER SCOPE; assignment rows decide which scope is active here.', globalSkills.length > 0 ? `GLOBAL SCOPE / Global context skills:\n${globalSkills.join('\n')}` : 'GLOBAL SCOPE / Global context skills: none.', projectSkills.length > 0 ? `PROJECT SCOPE / Project-scoped active skills:\n${projectSkills.join('\n')}` : 'PROJECT SCOPE / Project-scoped active skills: none.', userSkills.length > 0 ? `USER SCOPE / User-scoped active skills:\n${userSkills.join('\n')}` : 'USER SCOPE / User-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(' ');
848
864
  };
849
865
  const toWorkspaceResolutionOutput = resolution => ({
850
866
  requestedDirectory: resolution.requestedDirectory,
@@ -2290,6 +2306,7 @@ const OpencodeWizardSkillsPlugin = async input => {
2290
2306
  publishedSkillCount: catalog.publishedSkillCount.toString(),
2291
2307
  globalAssignmentCount: catalog.assignmentCounts.global.toString(),
2292
2308
  projectAssignmentCount: catalog.assignmentCounts.project.toString(),
2309
+ userAssignmentCount: catalog.assignmentCounts.user.toString(),
2293
2310
  ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
2294
2311
  }
2295
2312
  });
@@ -2312,6 +2329,9 @@ const OpencodeWizardSkillsPlugin = async input => {
2312
2329
  status: 'ready',
2313
2330
  ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2314
2331
  publishedSkillCount: catalog.publishedSkillCount.toString(),
2332
+ globalAssignmentCount: catalog.assignmentCounts.global.toString(),
2333
+ projectAssignmentCount: catalog.assignmentCounts.project.toString(),
2334
+ userAssignmentCount: catalog.assignmentCounts.user.toString(),
2315
2335
  ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
2316
2336
  }
2317
2337
  };