@aexol/opencode-wizard 0.3.1 → 0.3.3

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
@@ -50,7 +50,13 @@ Catalog discovery uses the backend-issued plugin session token stored at `~/.con
50
50
 
51
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.
52
52
 
53
- No-arg discovery returns published skill summaries, assignment counts split into `global` and `project`, policy metadata, and no markdown bodies. Existing `skill` and `skills` calls still fetch one or more full skill body/detail payloads by slug, artifact name, or skill name.
53
+ No-arg discovery returns published skill summaries, assignment counts split into `global`, `project`, `user`, and `other`, policy metadata, and no markdown bodies. Existing `skill` and `skills` calls still fetch one or more full skill body/detail payloads by slug, artifact name, or skill name.
54
+
55
+ Workspace delivery still follows the backend contract: the plugin sends `workspaceSlug` when it has one, otherwise falls back to `repositoryUrl`. The plugin now prefers configured or learned workspace slug mappings over a repo-basename fallback, and learns durable slug-to-repo mappings from successful backend catalog responses so worktrees and nontrivial repo roots stay aligned.
56
+
57
+ Published skill fetches still support `refresh: true`, but normal cache entries now self-expire after 30 seconds and fetch/status payloads surface `source`, `workspaceSlug`, and `workspaceSlugSource` so stale-vs-refreshed behavior is visible without relying on manual cache deletion.
58
+
59
+ Use `opencode_wizard_published_skill_preference_set` for non-TUI preference actions (`install`, `uninstall`, `ignore`, `unignore`) against the same server-backed preference API used by the TUI overlay.
54
60
 
55
61
  `GLOBAL_CONTEXT` skills are active context skills and are not meant to be installed per project. `PROJECT_INSTALLABLE` skills are gallery/installable skills that may be attached globally or to a workspace/path through assignment records; those assignments remain the source of truth for what is active in a catalog response.
56
62
 
package/dist/server.d.ts CHANGED
@@ -5,6 +5,7 @@ type ResolvedConfig = {
5
5
  authSessionUrl: string;
6
6
  presenceUrl: string;
7
7
  actionsUrl: string;
8
+ configuredWorkspaceSlug: string | null;
8
9
  fallbackWorkspaceSlug: string;
9
10
  rootSkillSeedPath: string;
10
11
  authStatePath: string;
@@ -13,6 +14,8 @@ type WorkspaceResolution = {
13
14
  requestedDirectory: string;
14
15
  repositoryRoot: string;
15
16
  repositoryUrl: string | null;
17
+ workspaceSlug?: string | null;
18
+ workspaceSlugSource?: 'configured' | 'learned' | 'backend' | 'fallback' | 'repositoryUrl' | 'placeholder';
16
19
  fallbackWorkspaceSlug: string | null;
17
20
  directoryPath: string;
18
21
  cacheKey: string;
@@ -271,6 +274,8 @@ declare const toWorkspaceResolutionOutput: (resolution: WorkspaceResolution) =>
271
274
  requestedDirectory: string;
272
275
  repositoryRoot: string;
273
276
  repositoryUrl: string | null;
277
+ workspaceSlug: string | null | undefined;
278
+ workspaceSlugSource: "configured" | "learned" | "backend" | "fallback" | "repositoryUrl" | "placeholder" | undefined;
274
279
  fallbackWorkspaceSlug: string | null;
275
280
  directoryPath: string;
276
281
  };
package/dist/server.js CHANGED
@@ -14,6 +14,7 @@ const MODULE_FILE_PATH = fileURLToPath(import.meta.url);
14
14
  const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
15
15
  export const PLUGIN_ID = 'opencode-wizard';
16
16
  const CACHE_TTL_MS = 30_000;
17
+ const WORKSPACE_MAPPING_LIMIT = 100;
17
18
  const ROOT_SKILL_SEED_PATH = '.opencode/skills';
18
19
  const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode-wizard.json');
19
20
  const LEGACY_AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
@@ -55,7 +56,7 @@ const statusPathLoginBootstrap = {
55
56
  failedAt: null
56
57
  };
57
58
  const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
58
- export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch', 'opencode_wizard_status'];
59
+ export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch', 'opencode_wizard_published_skill_preference_set', 'opencode_wizard_status'];
59
60
  let publishedSkillPreferenceCacheVersion = 0;
60
61
  export const NATIVE_SKILLS_URL_COMPATIBILITY = {
61
62
  configKey: 'skills.urls',
@@ -330,14 +331,19 @@ const resolveBackendOrigin = async worktree => {
330
331
  localBackendOrigin: envValues.get('OPENCODE_WIZARD_BACKEND_ORIGIN')
331
332
  });
332
333
  };
334
+ const readConfiguredWorkspaceSlug = () => {
335
+ const configuredWorkspaceSlug = process.env.OPENCODE_WIZARD_SKILLS_WORKSPACE_SLUG?.trim();
336
+ if (!configuredWorkspaceSlug) return null;
337
+ return toWorkspaceSlug(configuredWorkspaceSlug);
338
+ };
333
339
  const toWorkspaceSlug = value => {
334
340
  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '');
335
341
  if (normalized) return normalized;
336
342
  return 'workspace';
337
343
  };
338
344
  const resolveFallbackWorkspaceSlug = worktree => {
339
- const configuredWorkspaceSlug = process.env.OPENCODE_WIZARD_SKILLS_WORKSPACE_SLUG?.trim();
340
- if (configuredWorkspaceSlug) return toWorkspaceSlug(configuredWorkspaceSlug);
345
+ const configuredWorkspaceSlug = readConfiguredWorkspaceSlug();
346
+ if (configuredWorkspaceSlug) return configuredWorkspaceSlug;
341
347
  return toWorkspaceSlug(path.basename(path.resolve(worktree)));
342
348
  };
343
349
  export const resolveConfig = async worktree => {
@@ -348,6 +354,7 @@ export const resolveConfig = async worktree => {
348
354
  authSessionUrl: `${backendOrigin}/api/opencode-plugin/oauth/session`,
349
355
  presenceUrl: `${backendOrigin}/api/opencode-plugin/presence`,
350
356
  actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
357
+ configuredWorkspaceSlug: readConfiguredWorkspaceSlug(),
351
358
  fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
352
359
  rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
353
360
  authStatePath: GLOBAL_CONFIG_PATH
@@ -408,22 +415,31 @@ const resolveWorkspace = async ({
408
415
  const gitRoot = await resolveGitRoot(requestedDirectory);
409
416
  const repositoryRoot = gitRoot ?? requestedDirectory;
410
417
  const repositoryUrl = gitRoot ? await resolveGitRemoteOriginUrl(gitRoot) : null;
418
+ const learnedWorkspaceMapping = await findWorkspaceSlugMapping({
419
+ configFile: config.authStatePath,
420
+ repositoryUrl,
421
+ repositoryRoot
422
+ });
411
423
  const fallbackWorkspaceSlug = config.fallbackWorkspaceSlug;
412
424
  const directoryPath = normalizeRepositoryPath(repositoryRoot, requestedDirectory);
413
- const workspaceIdentity = `workspaceSlug:${fallbackWorkspaceSlug}`;
425
+ const workspaceSlug = config.configuredWorkspaceSlug ?? learnedWorkspaceMapping?.workspaceSlug ?? fallbackWorkspaceSlug ?? null;
426
+ const workspaceSlugSource = config.configuredWorkspaceSlug ? 'configured' : learnedWorkspaceMapping?.workspaceSlug ? 'learned' : fallbackWorkspaceSlug ? 'fallback' : repositoryUrl ? 'repositoryUrl' : 'placeholder';
427
+ const workspaceIdentity = workspaceSlug ? `workspaceSlug:${workspaceSlug}` : repositoryUrl ? `repository:${repositoryUrl}` : 'workspace:placeholder';
414
428
  return {
415
429
  requestedDirectory,
416
430
  repositoryRoot,
417
431
  repositoryUrl,
432
+ workspaceSlug,
433
+ workspaceSlugSource,
418
434
  fallbackWorkspaceSlug,
419
435
  directoryPath,
420
436
  cacheKey: JSON.stringify([workspaceIdentity, directoryPath])
421
437
  };
422
438
  };
423
439
  const toDeliveryInput = resolution => {
424
- if (resolution.fallbackWorkspaceSlug) {
440
+ if (resolution.workspaceSlug) {
425
441
  return {
426
- workspaceSlug: resolution.fallbackWorkspaceSlug,
442
+ workspaceSlug: resolution.workspaceSlug,
427
443
  directoryPath: resolution.directoryPath
428
444
  };
429
445
  }
@@ -475,6 +491,76 @@ const withoutLegacyPublishedSkillPreferences = config => {
475
491
  const hasLegacyPublishedSkillPreferences = config => {
476
492
  return Object.prototype.hasOwnProperty.call(config, 'publishedSkillPreferences') || Object.prototype.hasOwnProperty.call(config, 'ignoredPublishedSkills');
477
493
  };
494
+ const isStoredWorkspaceSlugMapping = value => {
495
+ if (!isRecord(value)) return false;
496
+ const {
497
+ repositoryUrl,
498
+ repositoryRoot,
499
+ workspaceSlug,
500
+ updatedAt
501
+ } = value;
502
+ const hasValidRepositoryUrl = repositoryUrl === null || typeof repositoryUrl === 'string';
503
+ const hasValidRepositoryRoot = repositoryRoot === null || typeof repositoryRoot === 'string';
504
+ return hasValidRepositoryUrl && hasValidRepositoryRoot && typeof workspaceSlug === 'string' && workspaceSlug.trim().length > 0 && isValidIsoDateString(updatedAt);
505
+ };
506
+ const readWorkspaceSlugMappings = async configFile => {
507
+ const storedConfig = await readGlobalConfig(configFile);
508
+ const mappings = storedConfig.workspaceSlugMappings;
509
+ if (!Array.isArray(mappings)) return [];
510
+ return mappings.filter(isStoredWorkspaceSlugMapping).slice(0, WORKSPACE_MAPPING_LIMIT);
511
+ };
512
+ const writeWorkspaceSlugMappings = async (configFile, nextMappings) => {
513
+ const storedConfig = await readGlobalConfig(configFile);
514
+ await writeGlobalConfig(configFile, {
515
+ ...withoutLegacyPublishedSkillPreferences(storedConfig),
516
+ workspaceSlugMappings: nextMappings.slice(0, WORKSPACE_MAPPING_LIMIT)
517
+ });
518
+ };
519
+ const normalizeStoredRepositoryRoot = value => {
520
+ if (!value) return null;
521
+ return normalizeAbsolutePath(value);
522
+ };
523
+ const upsertWorkspaceSlugMapping = async ({
524
+ configFile,
525
+ repositoryUrl,
526
+ repositoryRoot,
527
+ workspaceSlug
528
+ }) => {
529
+ const normalizedWorkspaceSlug = toWorkspaceSlug(workspaceSlug);
530
+ if (!normalizedWorkspaceSlug) return;
531
+ const normalizedRepositoryRoot = normalizeStoredRepositoryRoot(repositoryRoot);
532
+ const existingMappings = await readWorkspaceSlugMappings(configFile);
533
+ const filteredMappings = existingMappings.filter(mapping => {
534
+ if (repositoryUrl && mapping.repositoryUrl === repositoryUrl) return false;
535
+ if (normalizedRepositoryRoot && normalizeStoredRepositoryRoot(mapping.repositoryRoot) === normalizedRepositoryRoot) {
536
+ return false;
537
+ }
538
+ return true;
539
+ });
540
+ await writeWorkspaceSlugMappings(configFile, [{
541
+ repositoryUrl,
542
+ repositoryRoot: normalizedRepositoryRoot,
543
+ workspaceSlug: normalizedWorkspaceSlug,
544
+ updatedAt: new Date().toISOString()
545
+ }, ...filteredMappings]);
546
+ };
547
+ const findWorkspaceSlugMapping = async ({
548
+ configFile,
549
+ repositoryUrl,
550
+ repositoryRoot
551
+ }) => {
552
+ const normalizedRepositoryRoot = normalizeStoredRepositoryRoot(repositoryRoot);
553
+ const mappings = await readWorkspaceSlugMappings(configFile);
554
+ if (repositoryUrl) {
555
+ const repositoryMatch = mappings.find(mapping => mapping.repositoryUrl === repositoryUrl);
556
+ if (repositoryMatch) return repositoryMatch;
557
+ }
558
+ if (normalizedRepositoryRoot) {
559
+ const rootMatch = mappings.find(mapping => normalizeStoredRepositoryRoot(mapping.repositoryRoot) === normalizedRepositoryRoot);
560
+ if (rootMatch) return rootMatch;
561
+ }
562
+ return null;
563
+ };
478
564
  const readGlobalAuthState = async configFile => {
479
565
  const storedConfig = await readGlobalConfig(configFile);
480
566
  const storedAuthState = storedConfig.auth;
@@ -517,8 +603,21 @@ const toIgnoredSkillSlug = value => {
517
603
  if (!normalized) return null;
518
604
  return normalized;
519
605
  };
606
+ const toPublishedSkillPreferenceAction = value => {
607
+ const normalized = value.trim().toLowerCase();
608
+ if (normalized === 'install' || normalized === 'uninstall' || normalized === 'ignore' || normalized === 'unignore') {
609
+ return normalized;
610
+ }
611
+ throw new Error('Published skill preference action must be one of: install, uninstall, ignore, unignore.');
612
+ };
613
+ const toPublishedSkillPreferenceScope = (value, defaultScope) => {
614
+ if (!value) return defaultScope;
615
+ const normalized = value.trim().toLowerCase();
616
+ if (normalized === 'global' || normalized === 'project') return normalized;
617
+ throw new Error('Published skill preferenceScope must be either global or project.');
618
+ };
520
619
  const getPublishedSkillIgnoreScopeKey = (resolution, payload) => {
521
- const workspaceSlug = payload?.workspace?.slug ?? resolution.fallbackWorkspaceSlug;
620
+ const workspaceSlug = payload?.workspace?.slug ?? resolution.workspaceSlug ?? resolution.fallbackWorkspaceSlug;
522
621
  if (workspaceSlug) return `workspace:${toWorkspaceSlug(workspaceSlug)}`;
523
622
  if (resolution.repositoryUrl) return `repository:${resolution.repositoryUrl}`;
524
623
  return `path:${toWorkspaceSlug(path.basename(resolution.repositoryRoot))}`;
@@ -866,6 +965,8 @@ const toWorkspaceResolutionOutput = resolution => ({
866
965
  requestedDirectory: resolution.requestedDirectory,
867
966
  repositoryRoot: resolution.repositoryRoot,
868
967
  repositoryUrl: resolution.repositoryUrl,
968
+ workspaceSlug: resolution.workspaceSlug,
969
+ workspaceSlugSource: resolution.workspaceSlugSource,
869
970
  fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug,
870
971
  directoryPath: resolution.directoryPath
871
972
  });
@@ -873,6 +974,8 @@ const toWorkspaceResolutionMetadata = resolution => ({
873
974
  directoryPath: resolution.directoryPath,
874
975
  repositoryRoot: resolution.repositoryRoot,
875
976
  repositoryUrl: resolution.repositoryUrl ?? '',
977
+ workspaceSlug: resolution.workspaceSlug ?? '',
978
+ workspaceSlugSource: resolution.workspaceSlugSource ?? 'placeholder',
876
979
  fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug ?? ''
877
980
  });
878
981
  const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
@@ -1170,7 +1273,12 @@ const toPluginStatusMetadata = snapshot => ({
1170
1273
  pluginStatus: snapshot.status,
1171
1274
  authStatus: snapshot.authState.status,
1172
1275
  authEmail: snapshot.authState.email ?? '',
1173
- authUserId: snapshot.authState.userId ?? ''
1276
+ authUserId: snapshot.authState.userId ?? '',
1277
+ directoryPath: snapshot.workspaceResolution.directoryPath,
1278
+ repositoryUrl: snapshot.workspaceResolution.repositoryUrl ?? '',
1279
+ source: snapshot.source,
1280
+ workspaceSlug: snapshot.workspaceResolution.workspaceSlug ?? '',
1281
+ workspaceSlugSource: snapshot.workspaceResolution.workspaceSlugSource ?? 'placeholder'
1174
1282
  });
1175
1283
  const isUnauthorizedGraphQlMessage = message => {
1176
1284
  const normalizedMessage = message.toLowerCase();
@@ -1491,6 +1599,24 @@ const fetchPublishedSkillsCatalog = async (worktree, config, resolution, signal,
1491
1599
  source: 'network'
1492
1600
  };
1493
1601
  };
1602
+ const maybePersistWorkspaceSlugFromCatalog = async ({
1603
+ config,
1604
+ resolution,
1605
+ fetchResult
1606
+ }) => {
1607
+ if (!fetchResult.ok) return;
1608
+ const backendWorkspaceSlug = fetchResult.payload.workspace?.slug?.trim();
1609
+ if (!backendWorkspaceSlug) return;
1610
+ const normalizedWorkspaceSlug = toWorkspaceSlug(backendWorkspaceSlug);
1611
+ if (!normalizedWorkspaceSlug) return;
1612
+ if (resolution.workspaceSlug === normalizedWorkspaceSlug && resolution.workspaceSlugSource !== 'fallback') return;
1613
+ await upsertWorkspaceSlugMapping({
1614
+ configFile: config.authStatePath,
1615
+ repositoryUrl: resolution.repositoryUrl,
1616
+ repositoryRoot: resolution.repositoryRoot,
1617
+ workspaceSlug: normalizedWorkspaceSlug
1618
+ });
1619
+ };
1494
1620
  const fetchPublishedSkillDetail = async ({
1495
1621
  worktree,
1496
1622
  config,
@@ -1928,6 +2054,11 @@ const OpencodeWizardSkillsPlugin = async input => {
1928
2054
  }
1929
2055
  const requestPromise = (async () => {
1930
2056
  const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
2057
+ await maybePersistWorkspaceSlugFromCatalog({
2058
+ config,
2059
+ resolution: workspaceResolution,
2060
+ fetchResult
2061
+ });
1931
2062
  cache.set(cacheKey, {
1932
2063
  result: fetchResult,
1933
2064
  expiresAt: Date.now() + CACHE_TTL_MS
@@ -1958,7 +2089,7 @@ const OpencodeWizardSkillsPlugin = async input => {
1958
2089
  const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
1959
2090
  const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
1960
2091
  const cached = detailCache.get(cacheKey);
1961
- if (purpose === 'SYSTEM_CONTEXT' && useCache && cached && cached.expiresAt > Date.now()) {
2092
+ if (useCache && cached && cached.expiresAt > Date.now()) {
1962
2093
  return {
1963
2094
  ok: true,
1964
2095
  detail: toPublishedSkillDetail({
@@ -2135,11 +2266,13 @@ const OpencodeWizardSkillsPlugin = async input => {
2135
2266
  },
2136
2267
  fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2137
2268
  source: filteredPublishedSkillsResult.fetchResult.source,
2138
- message: 'Catalog discovery only. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.'
2269
+ cacheTtlMs: CACHE_TTL_MS,
2270
+ message: args.refresh ? 'Catalog discovery refreshed from the backend. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.' : 'Catalog discovery only. Cached results expire automatically after 30 seconds, or pass `refresh: true` to force a backend refresh immediately. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.'
2139
2271
  }, null, 2),
2140
2272
  metadata: {
2141
2273
  status: 'ready',
2142
2274
  ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2275
+ source: filteredPublishedSkillsResult.fetchResult.source,
2143
2276
  publishedSkillCount: catalog.publishedSkillCount.toString(),
2144
2277
  globalAssignmentCount: catalog.assignmentCounts.global.toString(),
2145
2278
  projectAssignmentCount: catalog.assignmentCounts.project.toString(),
@@ -2229,6 +2362,7 @@ const OpencodeWizardSkillsPlugin = async input => {
2229
2362
  metadata: {
2230
2363
  status: 'ready',
2231
2364
  ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2365
+ source: filteredPublishedSkillsResult.fetchResult.source,
2232
2366
  skillSlug: detail.skillSlug
2233
2367
  }
2234
2368
  };
@@ -2258,6 +2392,7 @@ const OpencodeWizardSkillsPlugin = async input => {
2258
2392
  metadata: {
2259
2393
  status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
2260
2394
  ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2395
+ source: filteredPublishedSkillsResult.fetchResult.source,
2261
2396
  matchedCount: skillDetails.length.toString()
2262
2397
  }
2263
2398
  };
@@ -2286,6 +2421,9 @@ const OpencodeWizardSkillsPlugin = async input => {
2286
2421
  // Keep returning the safe missing-auth snapshot when interactive login is cancelled or fails.
2287
2422
  }
2288
2423
  }
2424
+ if (snapshot.status === 'ready') {
2425
+ await scheduleInteractivePresenceStart();
2426
+ }
2289
2427
  const metadata = toPluginStatusMetadata(snapshot);
2290
2428
  context.metadata({
2291
2429
  title: `opencode-wizard status: ${snapshot.status} / auth ${snapshot.authState.status}`,
@@ -2296,6 +2434,96 @@ const OpencodeWizardSkillsPlugin = async input => {
2296
2434
  metadata
2297
2435
  };
2298
2436
  };
2437
+ const executePublishedSkillPreferenceTool = async ({
2438
+ args,
2439
+ context
2440
+ }) => {
2441
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
2442
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
2443
+ lastInteractiveDirectoryPath = directoryPath;
2444
+ const requestedSkill = typeof args.skill === 'string' ? args.skill.trim() : '';
2445
+ const emitPreferenceOutcome = async event => {
2446
+ await emitActionEventForCurrentSession({
2447
+ event,
2448
+ directoryPath
2449
+ });
2450
+ };
2451
+ try {
2452
+ if (!requestedSkill) {
2453
+ throw new Error('Published skill preference tool requires a non-empty skill slug, artifact name, or skill name.');
2454
+ }
2455
+ if (typeof args.action !== 'string') {
2456
+ throw new Error('Published skill preference tool requires an action: install, uninstall, ignore, or unignore.');
2457
+ }
2458
+ const action = toPublishedSkillPreferenceAction(args.action);
2459
+ const catalogResult = await loadPublishedSkillCatalog({
2460
+ directory: requestedDirectory,
2461
+ useCache: true,
2462
+ signal: context.abort
2463
+ });
2464
+ if (!catalogResult.fetchResult.ok) {
2465
+ throw new Error(`Cannot resolve published skill preference target: ${catalogResult.fetchResult.message}`);
2466
+ }
2467
+ const selectableCatalogSkills = catalogResult.fetchResult.payload.catalogSkills.map(item => ({
2468
+ ...item,
2469
+ assignmentSource: 'CATALOG',
2470
+ assignmentType: 'PATH',
2471
+ scopePath: '',
2472
+ includeChildren: true
2473
+ }));
2474
+ const preferenceSelection = selectPublishedSkills({
2475
+ ...catalogResult.fetchResult.payload,
2476
+ skills: [...catalogResult.fetchResult.payload.skills, ...selectableCatalogSkills, ...catalogResult.fetchResult.payload.userPreferences.ignoredSkills]
2477
+ }, [requestedSkill]);
2478
+ const matchedSkill = preferenceSelection.selectedItems[0];
2479
+ if (!matchedSkill) {
2480
+ throw new Error(`Published skill preference target was not found for identifier: ${requestedSkill}.`);
2481
+ }
2482
+ const skillSlug = matchedSkill.skill.slug;
2483
+ const preferenceState = action === 'ignore' || action === 'unignore' ? await setPublishedSkillIgnored({
2484
+ worktree: input.worktree,
2485
+ directory: requestedDirectory,
2486
+ skillSlug,
2487
+ ignored: action === 'ignore',
2488
+ preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project')
2489
+ }) : await setPublishedSkillInstalled({
2490
+ worktree: input.worktree,
2491
+ directory: requestedDirectory,
2492
+ skillSlug,
2493
+ installed: action === 'install',
2494
+ preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project')
2495
+ });
2496
+ await scheduleInteractivePresenceStart();
2497
+ await emitPreferenceOutcome('PREFERENCE_SUCCESS');
2498
+ const metadata = {
2499
+ status: 'updated',
2500
+ skillSlug,
2501
+ action,
2502
+ directoryPath,
2503
+ ignoredSkillCount: preferenceState.ignoredSkillSlugs.length.toString()
2504
+ };
2505
+ context.metadata({
2506
+ title: `opencode-wizard published skill preference: ${action} ${skillSlug}`,
2507
+ metadata
2508
+ });
2509
+ return {
2510
+ output: JSON.stringify({
2511
+ pluginId: PLUGIN_ID,
2512
+ status: 'updated',
2513
+ requestedIdentifier: requestedSkill,
2514
+ skillSlug,
2515
+ action,
2516
+ requestedDirectoryPath: directoryPath,
2517
+ preferenceState,
2518
+ message: 'Published skill preference updated through the shared server-backed API; TUI views will reflect this after refresh.'
2519
+ }, null, 2),
2520
+ metadata
2521
+ };
2522
+ } catch (error) {
2523
+ await emitPreferenceOutcome('PREFERENCE_FAILED');
2524
+ throw error;
2525
+ }
2526
+ };
2299
2527
  return {
2300
2528
  tool: {
2301
2529
  opencode_wizard_published_skills_fetch: tool({
@@ -2313,6 +2541,21 @@ const OpencodeWizardSkillsPlugin = async input => {
2313
2541
  });
2314
2542
  }
2315
2543
  }),
2544
+ opencode_wizard_published_skill_preference_set: tool({
2545
+ description: 'Install, uninstall, ignore, or unignore a backend-published wizard skill for non-TUI workflows using the same shared server-backed preference API as the TUI overlay',
2546
+ args: {
2547
+ skill: tool.schema.string().describe('Published skill slug, artifact name, or skill name to update'),
2548
+ action: tool.schema.string().describe('Preference action: install, uninstall, ignore, or unignore'),
2549
+ preferenceScope: tool.schema.string().optional().describe('Preference scope for the action: project or global; defaults to project'),
2550
+ directory: tool.schema.string().optional().describe('Optional absolute or relative directory override')
2551
+ },
2552
+ async execute(args, context) {
2553
+ return executePublishedSkillPreferenceTool({
2554
+ args,
2555
+ context
2556
+ });
2557
+ }
2558
+ }),
2316
2559
  opencode_wizard_status: tool({
2317
2560
  description: 'Report opencode-wizard plugin status, bootstrap auth when missing, and return a safe auth summary without exposing tokens',
2318
2561
  args: {
@@ -2352,6 +2595,9 @@ const OpencodeWizardSkillsPlugin = async input => {
2352
2595
  return;
2353
2596
  }
2354
2597
  }
2598
+ if (publishedSkillsResult.fetchResult.ok) {
2599
+ await scheduleInteractivePresenceStart();
2600
+ }
2355
2601
  const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
2356
2602
  const details = await loadSystemNoteDetails({
2357
2603
  publishedSkillsResult: filteredPublishedSkillsResult,