@aexol/opencode-wizard 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +9 -7
  2. package/dist/graphql-operations.d.ts +7 -0
  3. package/dist/graphql-operations.js +230 -0
  4. package/dist/graphql-operations.js.map +1 -0
  5. package/dist/plugin-tools.d.ts +90 -0
  6. package/dist/plugin-tools.js +93 -0
  7. package/dist/plugin-tools.js.map +1 -0
  8. package/dist/published-skills-system-note.d.ts +9 -0
  9. package/dist/published-skills-system-note.js +30 -0
  10. package/dist/published-skills-system-note.js.map +1 -0
  11. package/dist/published-skills-terminology.d.ts +21 -0
  12. package/dist/published-skills-terminology.js +38 -0
  13. package/dist/published-skills-terminology.js.map +1 -0
  14. package/dist/published-skills-transform.d.ts +258 -0
  15. package/dist/published-skills-transform.js +310 -0
  16. package/dist/published-skills-transform.js.map +1 -0
  17. package/dist/server/auth-bootstrap.d.ts +7 -0
  18. package/dist/server/auth-bootstrap.js +89 -0
  19. package/dist/server/auth-bootstrap.js.map +1 -0
  20. package/dist/server/auth-flow.d.ts +10 -0
  21. package/dist/server/auth-flow.js +215 -0
  22. package/dist/server/auth-flow.js.map +1 -0
  23. package/dist/server/auth-store.d.ts +19 -0
  24. package/dist/server/auth-store.js +177 -0
  25. package/dist/server/auth-store.js.map +1 -0
  26. package/dist/server/client.d.ts +80 -0
  27. package/dist/server/client.js +324 -0
  28. package/dist/server/client.js.map +1 -0
  29. package/dist/server/config.d.ts +2 -0
  30. package/dist/server/config.js +82 -0
  31. package/dist/server/config.js.map +1 -0
  32. package/dist/server/constants.d.ts +26 -0
  33. package/dist/server/constants.js +32 -0
  34. package/dist/server/constants.js.map +1 -0
  35. package/dist/server/path-utils.d.ts +2 -0
  36. package/dist/server/path-utils.js +8 -0
  37. package/dist/server/path-utils.js.map +1 -0
  38. package/dist/server/preferences.d.ts +22 -0
  39. package/dist/server/preferences.js +121 -0
  40. package/dist/server/preferences.js.map +1 -0
  41. package/dist/server/presence.d.ts +14 -0
  42. package/dist/server/presence.js +68 -0
  43. package/dist/server/presence.js.map +1 -0
  44. package/dist/server/runtime.d.ts +13 -0
  45. package/dist/server/runtime.js +1315 -0
  46. package/dist/server/runtime.js.map +1 -0
  47. package/dist/server/status.d.ts +27 -0
  48. package/dist/server/status.js +224 -0
  49. package/dist/server/status.js.map +1 -0
  50. package/dist/server/types.d.ts +396 -0
  51. package/dist/server/types.js +2 -0
  52. package/dist/server/types.js.map +1 -0
  53. package/dist/server/workspace.d.ts +15 -0
  54. package/dist/server/workspace.js +126 -0
  55. package/dist/server/workspace.js.map +1 -0
  56. package/dist/server.d.ts +4 -309
  57. package/dist/server.js +4 -2611
  58. package/dist/server.js.map +1 -1
  59. package/dist/smoke-published-skills.js +11 -9
  60. package/dist/smoke-published-skills.js.map +1 -1
  61. package/dist/tui/components/common.d.ts +15 -0
  62. package/dist/tui/components/common.js +81 -0
  63. package/dist/tui/components/common.js.map +1 -0
  64. package/dist/tui/components/preference-action-notice-row.d.ts +5 -0
  65. package/dist/tui/components/preference-action-notice-row.js +17 -0
  66. package/dist/tui/components/preference-action-notice-row.js.map +1 -0
  67. package/dist/tui/components/skill-catalog-row.d.ts +8 -0
  68. package/dist/tui/components/skill-catalog-row.js +125 -0
  69. package/dist/tui/components/skill-catalog-row.js.map +1 -0
  70. package/dist/tui/components/status-content.d.ts +14 -0
  71. package/dist/tui/components/status-content.js +131 -0
  72. package/dist/tui/components/status-content.js.map +1 -0
  73. package/dist/tui/components/wizard-skills-dialog-content.d.ts +9 -0
  74. package/dist/tui/components/wizard-skills-dialog-content.js +229 -0
  75. package/dist/tui/components/wizard-skills-dialog-content.js.map +1 -0
  76. package/dist/tui/components/wizard-skills-dialog.d.ts +7 -0
  77. package/dist/tui/components/wizard-skills-dialog.js +156 -0
  78. package/dist/tui/components/wizard-skills-dialog.js.map +1 -0
  79. package/dist/tui/constants.d.ts +8 -0
  80. package/dist/tui/constants.js +9 -0
  81. package/dist/tui/constants.js.map +1 -0
  82. package/dist/tui/formatting.d.ts +8 -0
  83. package/dist/tui/formatting.js +45 -0
  84. package/dist/tui/formatting.js.map +1 -0
  85. package/dist/tui/plugin.d.ts +2 -0
  86. package/dist/tui/plugin.js +26 -0
  87. package/dist/tui/plugin.js.map +1 -0
  88. package/dist/tui/rendering.d.ts +2 -0
  89. package/dist/tui/rendering.js +8 -0
  90. package/dist/tui/rendering.js.map +1 -0
  91. package/dist/tui/skill-helpers.d.ts +13 -0
  92. package/dist/tui/skill-helpers.js +50 -0
  93. package/dist/tui/skill-helpers.js.map +1 -0
  94. package/dist/tui/slots.d.ts +2 -0
  95. package/dist/tui/slots.js +56 -0
  96. package/dist/tui/slots.js.map +1 -0
  97. package/dist/tui/status.d.ts +2 -0
  98. package/dist/tui/status.js +21 -0
  99. package/dist/tui/status.js.map +1 -0
  100. package/dist/tui/types.d.ts +75 -0
  101. package/dist/tui/types.js +2 -0
  102. package/dist/tui/types.js.map +1 -0
  103. package/dist/tui.d.ts +1 -44
  104. package/dist/tui.js +2 -870
  105. package/dist/tui.js.map +1 -1
  106. package/package.json +1 -1
@@ -0,0 +1,1315 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { CREATE_OR_UPDATE_SKILL_FROM_MARKDOWN_MUTATION, SET_WIZARD_ARTIFACT_PREFERENCE_MUTATION } from '../graphql-operations.js';
4
+ import { createPublishedSkillToolDefinitions, resolveAvailableTools } from '../plugin-tools.js';
5
+ import { resolveStoredAuthState, toAuthState, writeAuthState } from './auth-store.js';
6
+ import { resolveConfig } from './config.js';
7
+ export { resolveConfig } from './config.js';
8
+ import { createPluginSession, openBrowser, startLoginFlow } from './auth-flow.js';
9
+ import { fetchPublishedSkillDetail, fetchPublishedSkillsCatalog, fetchPublishedSkillsGraphQl, fetchWizardArtifactDetail, fetchWizardArtifactsCatalog, maybePersistWorkspaceSlugFromCatalog } from './client.js';
10
+ import { normalizeAbsolutePath } from './path-utils.js';
11
+ import { emitPluginActionEvent, emitPresenceEvent } from './presence.js';
12
+ import { normalizeDirectoryArg, normalizeRepositoryPath, resolveWorkspace, toWorkspaceResolutionMetadata, toWorkspaceResolutionOutput } from './workspace.js';
13
+ import { parseRequestedSkillArgs, selectPublishedSkills, toPublishedSkillDetail, toPublishedSkillSummary, toWizardArtifactCatalog, toWizardArtifactDetail, toWizardArtifactSummary } from '../published-skills-transform.js';
14
+ import { CACHE_TTL_MS, LOGIN_TIMEOUT_MS, OIDC_CALLBACK_URL, PLUGIN_ID, PRESENCE_SHUTDOWN_SIGNALS, PRESENCE_SIGNAL_EXIT_CODES } from './constants.js';
15
+ export { PLUGIN_ID, NATIVE_SKILLS_URL_COMPATIBILITY } from './constants.js';
16
+ import { buildSystemNote, filterIgnoredPublishedSkills, resolvePluginStatusSnapshot, toAiFacingPluginStatusSnapshot, toFetchFailureOutput, toPluginStatusMetadata, toPublishedSkillCatalog } from './status.js';
17
+ import { createIdleLoginBootstrapSnapshot } from './auth-bootstrap.js';
18
+ import { getCatalogCacheKey, resolvePublishedSkillPreferenceCacheContext, setPublishedSkillIgnored, setPublishedSkillInstalled, toPublishedSkillPreferenceAction, toPublishedSkillPreferenceScope } from './preferences.js';
19
+ export { buildSystemNote, resolvePluginStatusSnapshot, toPluginAuthStateSummary, toPublishedSkillCatalog } from './status.js';
20
+ const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
21
+ export { resolvePluginStatusSnapshotWithAuthBootstrap } from './auth-bootstrap.js';
22
+ export { setPublishedSkillIgnored, setPublishedSkillInstalled } from './preferences.js';
23
+ const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
24
+ return JSON.stringify([catalogCacheKey, skillVersionId]);
25
+ };
26
+ const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
27
+ return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
28
+ };
29
+ const SUPPORTED_WIZARD_ARTIFACT_KINDS = ['SKILL', 'DESIGN_DOC'];
30
+ const DESIGN_DOC_UNSUPPORTED_MESSAGE = 'DESIGN_DOC is supported by generic wizard artifact persistence; use catalog/detail tools for assigned or installed documents.';
31
+ const toWizardArtifactKind = value => {
32
+ if (!value) return 'SKILL';
33
+ const normalized = value.trim().toUpperCase().replace(/[-\s]+/gu, '_');
34
+ if (normalized === 'SKILL') return 'SKILL';
35
+ if (normalized === 'DESIGN_DOC') return 'DESIGN_DOC';
36
+ return null;
37
+ };
38
+ const buildUnsupportedWizardArtifactOutput = ({
39
+ artifactKind,
40
+ directoryPath
41
+ }) => ({
42
+ output: JSON.stringify({
43
+ pluginId: PLUGIN_ID,
44
+ runtimeMode: 'tool_fetch_only',
45
+ status: artifactKind === 'DESIGN_DOC' ? 'unsupported' : 'bad_artifact_kind',
46
+ artifactKind,
47
+ supportedArtifactKinds: SUPPORTED_WIZARD_ARTIFACT_KINDS,
48
+ requestedDirectoryPath: directoryPath,
49
+ message: artifactKind === 'DESIGN_DOC' ? DESIGN_DOC_UNSUPPORTED_MESSAGE : 'Unsupported wizard artifact kind. Use SKILL or DESIGN_DOC.'
50
+ }, null, 2),
51
+ metadata: {
52
+ status: artifactKind === 'DESIGN_DOC' ? 'unsupported' : 'bad_artifact_kind',
53
+ artifactKind,
54
+ directoryPath
55
+ }
56
+ });
57
+ const withWizardArtifactEnvelope = (result, artifactKind) => {
58
+ if (!result || typeof result !== 'object' || !('output' in result)) return result;
59
+ const outputResult = result;
60
+ if (typeof outputResult.output !== 'string') return result;
61
+ try {
62
+ return {
63
+ ...outputResult,
64
+ output: JSON.stringify({
65
+ ...JSON.parse(outputResult.output),
66
+ artifactKind,
67
+ supportedArtifactKinds: SUPPORTED_WIZARD_ARTIFACT_KINDS,
68
+ compatibilityAliases: ['opencode_wizard_published_skills_fetch', 'opencode_wizard_published_skill_preference_set']
69
+ }, null, 2),
70
+ metadata: {
71
+ ...(outputResult.metadata ?? {}),
72
+ artifactKind
73
+ }
74
+ };
75
+ } catch {
76
+ return result;
77
+ }
78
+ };
79
+ const matchesWizardArtifactIdentifier = (item, identifier) => {
80
+ const normalizedIdentifier = identifier.trim().toLowerCase();
81
+ if (!normalizedIdentifier) return false;
82
+ if (item.artifact.slug.toLowerCase() === normalizedIdentifier) return true;
83
+ if (item.artifact.name.toLowerCase() === normalizedIdentifier) return true;
84
+ if (item.artifactVersion.frontmatterName.toLowerCase() === normalizedIdentifier) return true;
85
+ return item.artifactVersion.id.toLowerCase() === normalizedIdentifier;
86
+ };
87
+ const selectWizardArtifacts = (items, identifiers) => {
88
+ const selectedItems = items.filter(item => identifiers.some(identifier => matchesWizardArtifactIdentifier(item, identifier)));
89
+ const missingIdentifiers = identifiers.filter(identifier => !selectedItems.some(item => matchesWizardArtifactIdentifier(item, identifier)));
90
+ return {
91
+ selectedItems,
92
+ missingIdentifiers
93
+ };
94
+ };
95
+ export const OpencodeWizardSkillsPlugin = async input => {
96
+ const {
97
+ tool
98
+ } = await importOpencodePluginModule('@opencode-ai/plugin');
99
+ const config = await resolveConfig(input.worktree);
100
+ const workspacePath = normalizeAbsolutePath(input.worktree);
101
+ const cache = new Map();
102
+ const catalogInflight = new Map();
103
+ const detailCache = new Map();
104
+ const detailInflight = new Map();
105
+ const initialAuthState = await resolveStoredAuthState(input.worktree, config);
106
+ const loginBootstrap = {
107
+ promise: null,
108
+ snapshot: createIdleLoginBootstrapSnapshot()
109
+ };
110
+ let lastAuthenticatedAuthState = initialAuthState;
111
+ let didEmitStart = false;
112
+ let didScheduleStop = false;
113
+ let presenceStartPromise = null;
114
+ let presenceStopPromise = null;
115
+ let lastInteractiveDirectoryPath = null;
116
+ const resolveActionAuthState = async () => {
117
+ const storedAuthState = await resolveStoredAuthState(input.worktree, config);
118
+ if (storedAuthState) return storedAuthState;
119
+ return lastAuthenticatedAuthState;
120
+ };
121
+ const emitActionEventForCurrentSession = async ({
122
+ event,
123
+ authState,
124
+ directoryPath
125
+ }) => {
126
+ await emitPluginActionEvent({
127
+ config,
128
+ authState: authState ?? (await resolveActionAuthState()),
129
+ event,
130
+ workspacePath,
131
+ directoryPath
132
+ });
133
+ };
134
+ const schedulePresenceStart = authState => {
135
+ lastAuthenticatedAuthState = authState;
136
+ if (didEmitStart) {
137
+ return presenceStartPromise ?? Promise.resolve();
138
+ }
139
+ didEmitStart = true;
140
+ presenceStartPromise = Promise.all([emitPresenceEvent({
141
+ config,
142
+ authState,
143
+ event: 'START',
144
+ workspacePath
145
+ }), emitActionEventForCurrentSession({
146
+ event: 'START',
147
+ authState,
148
+ directoryPath: lastInteractiveDirectoryPath ?? undefined
149
+ })]).then(() => undefined);
150
+ return presenceStartPromise;
151
+ };
152
+ const schedulePresenceStop = () => {
153
+ if (didScheduleStop) {
154
+ return presenceStopPromise ?? Promise.resolve();
155
+ }
156
+ didScheduleStop = true;
157
+ if (!didEmitStart || !lastAuthenticatedAuthState) {
158
+ presenceStopPromise = Promise.resolve();
159
+ return presenceStopPromise;
160
+ }
161
+ presenceStopPromise = (async () => {
162
+ await presenceStartPromise?.catch(() => undefined);
163
+ await Promise.all([emitPresenceEvent({
164
+ config,
165
+ authState: lastAuthenticatedAuthState,
166
+ event: 'STOP',
167
+ workspacePath
168
+ }), emitActionEventForCurrentSession({
169
+ event: 'STOP',
170
+ authState: lastAuthenticatedAuthState,
171
+ directoryPath: lastInteractiveDirectoryPath ?? undefined
172
+ })]);
173
+ })();
174
+ return presenceStopPromise;
175
+ };
176
+ const scheduleInteractivePresenceStart = async () => {
177
+ const authState = await resolveStoredAuthState(input.worktree, config);
178
+ if (!authState) return;
179
+ await schedulePresenceStart(authState);
180
+ };
181
+ process.once('beforeExit', () => {
182
+ void schedulePresenceStop();
183
+ });
184
+ for (const shutdownSignal of PRESENCE_SHUTDOWN_SIGNALS) {
185
+ try {
186
+ process.once(shutdownSignal, () => {
187
+ void schedulePresenceStop().finally(() => {
188
+ process.exit(PRESENCE_SIGNAL_EXIT_CODES[shutdownSignal]);
189
+ });
190
+ });
191
+ } catch {
192
+ continue;
193
+ }
194
+ }
195
+ const clearPublishedSkillState = () => {
196
+ cache.clear();
197
+ catalogInflight.clear();
198
+ detailCache.clear();
199
+ detailInflight.clear();
200
+ };
201
+ const persistAuthState = async session => {
202
+ const authState = toAuthState(session);
203
+ await writeAuthState(config.authStatePath, authState);
204
+ clearPublishedSkillState();
205
+ return authState;
206
+ };
207
+ const startLoginCompletion = trigger => {
208
+ if (loginBootstrap.promise) {
209
+ return loginBootstrap.promise;
210
+ }
211
+ const startedAt = new Date().toISOString();
212
+ loginBootstrap.snapshot = {
213
+ status: 'starting',
214
+ trigger,
215
+ startedAt,
216
+ expiresAt: null,
217
+ browserUrl: null,
218
+ browserOpenError: null,
219
+ email: null,
220
+ message: null
221
+ };
222
+ const loginPromise = (async () => {
223
+ const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
224
+ let loginStart = null;
225
+ try {
226
+ loginStart = await startLoginFlow(loginSignal);
227
+ const browserOpenError = await openBrowser(loginStart.browserUrl);
228
+ loginBootstrap.snapshot = {
229
+ status: 'pending',
230
+ trigger,
231
+ startedAt,
232
+ expiresAt: loginStart.expiresAt,
233
+ browserUrl: loginStart.browserUrl,
234
+ browserOpenError,
235
+ email: null,
236
+ message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
237
+ };
238
+ const callbackPayload = await loginStart.callbackPromise;
239
+ if (callbackPayload.status === 'error') {
240
+ throw new Error(callbackPayload.message);
241
+ }
242
+ if (callbackPayload.state !== loginStart.expectedState) {
243
+ throw new Error('OAuth callback state did not match the original login request.');
244
+ }
245
+ loginBootstrap.snapshot = {
246
+ status: 'pending',
247
+ trigger,
248
+ startedAt,
249
+ expiresAt: loginStart.expiresAt,
250
+ browserUrl: loginStart.browserUrl,
251
+ browserOpenError,
252
+ email: null,
253
+ message: 'OAuth callback received. Finalizing backend session exchange.'
254
+ };
255
+ const pluginSession = await createPluginSession({
256
+ code: callbackPayload.code,
257
+ codeVerifier: loginStart.codeVerifier,
258
+ redirectUri: OIDC_CALLBACK_URL,
259
+ config,
260
+ signal: loginSignal
261
+ });
262
+ const authState = await persistAuthState(pluginSession);
263
+ await emitActionEventForCurrentSession({
264
+ event: 'LOGIN_SUCCESS',
265
+ authState,
266
+ directoryPath: lastInteractiveDirectoryPath ?? undefined
267
+ });
268
+ loginBootstrap.snapshot = {
269
+ status: 'authenticated',
270
+ trigger,
271
+ startedAt,
272
+ expiresAt: authState.expiresAt,
273
+ browserUrl: loginStart.browserUrl,
274
+ browserOpenError,
275
+ email: authState.email,
276
+ message: `Browser login completed successfully for published skill ${trigger}.`
277
+ };
278
+ return authState;
279
+ } catch (error) {
280
+ await emitActionEventForCurrentSession({
281
+ event: 'LOGIN_FAILED',
282
+ directoryPath: lastInteractiveDirectoryPath ?? undefined
283
+ });
284
+ loginBootstrap.snapshot = {
285
+ status: 'failed',
286
+ trigger,
287
+ startedAt,
288
+ expiresAt: loginBootstrap.snapshot.expiresAt,
289
+ browserUrl: loginBootstrap.snapshot.browserUrl,
290
+ browserOpenError: loginBootstrap.snapshot.browserOpenError,
291
+ email: null,
292
+ message: error instanceof Error ? error.message : 'Browser login failed.'
293
+ };
294
+ throw error;
295
+ } finally {
296
+ await loginStart?.closeCallbackServer().catch(() => undefined);
297
+ loginBootstrap.promise = null;
298
+ }
299
+ })();
300
+ loginBootstrap.promise = loginPromise;
301
+ return loginPromise;
302
+ };
303
+ const loadPublishedSkillCatalog = async ({
304
+ directory,
305
+ useCache,
306
+ signal
307
+ }) => {
308
+ const workspaceResolution = await resolveWorkspace({
309
+ config,
310
+ directory
311
+ });
312
+ const directoryPath = workspaceResolution.directoryPath;
313
+ const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
314
+ const cacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
315
+ const cached = cache.get(cacheKey);
316
+ if (useCache && cached && cached.expiresAt > Date.now()) {
317
+ return {
318
+ directoryPath,
319
+ workspaceResolution,
320
+ fetchResult: {
321
+ ...cached.result,
322
+ source: 'cache'
323
+ }
324
+ };
325
+ }
326
+ const inflight = catalogInflight.get(cacheKey);
327
+ if (inflight) {
328
+ return inflight;
329
+ }
330
+ const requestPromise = (async () => {
331
+ const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
332
+ await maybePersistWorkspaceSlugFromCatalog({
333
+ config,
334
+ resolution: workspaceResolution,
335
+ fetchResult
336
+ });
337
+ cache.set(cacheKey, {
338
+ result: fetchResult,
339
+ expiresAt: Date.now() + CACHE_TTL_MS
340
+ });
341
+ return {
342
+ directoryPath,
343
+ workspaceResolution,
344
+ fetchResult
345
+ };
346
+ })();
347
+ catalogInflight.set(cacheKey, requestPromise);
348
+ try {
349
+ return await requestPromise;
350
+ } finally {
351
+ catalogInflight.delete(cacheKey);
352
+ }
353
+ };
354
+ const loadPublishedSkillDetail = async ({
355
+ workspaceResolution,
356
+ item,
357
+ signal,
358
+ useCache,
359
+ purpose
360
+ }) => {
361
+ const directoryPath = workspaceResolution.directoryPath;
362
+ const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
363
+ const catalogCacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
364
+ const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
365
+ const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
366
+ const cached = detailCache.get(cacheKey);
367
+ if (useCache && cached && cached.expiresAt > Date.now()) {
368
+ return {
369
+ ok: true,
370
+ detail: toPublishedSkillDetail({
371
+ ...item,
372
+ publishedArtifact: cached.artifact
373
+ })
374
+ };
375
+ }
376
+ const inflight = detailInflight.get(inflightKey);
377
+ if (inflight) {
378
+ return inflight;
379
+ }
380
+ const requestPromise = (async () => {
381
+ const detailResult = await fetchPublishedSkillDetail({
382
+ worktree: input.worktree,
383
+ config,
384
+ resolution: workspaceResolution,
385
+ skillVersionId: item.skillVersion.id,
386
+ signal,
387
+ onAuthStateChanged: clearPublishedSkillState,
388
+ purpose
389
+ });
390
+ if (!detailResult.ok) {
391
+ return {
392
+ ok: false,
393
+ status: detailResult.result.status,
394
+ output: JSON.stringify({
395
+ pluginId: PLUGIN_ID,
396
+ runtimeMode: 'tool_fetch_only',
397
+ status: detailResult.result.status,
398
+ requestedDirectoryPath: directoryPath,
399
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
400
+ requestedSkillVersionId: item.skillVersion.id,
401
+ message: detailResult.result.message,
402
+ fetchedAt: detailResult.result.fetchedAt,
403
+ source: detailResult.result.source
404
+ }, null, 2),
405
+ metadata: {
406
+ status: detailResult.result.status,
407
+ ...toWorkspaceResolutionMetadata(workspaceResolution),
408
+ source: detailResult.result.source
409
+ }
410
+ };
411
+ }
412
+ detailCache.set(cacheKey, {
413
+ artifact: detailResult.artifact,
414
+ expiresAt: Date.now() + CACHE_TTL_MS
415
+ });
416
+ return {
417
+ ok: true,
418
+ detail: toPublishedSkillDetail({
419
+ ...item,
420
+ publishedArtifact: detailResult.artifact
421
+ })
422
+ };
423
+ })();
424
+ detailInflight.set(inflightKey, requestPromise);
425
+ try {
426
+ return await requestPromise;
427
+ } finally {
428
+ detailInflight.delete(inflightKey);
429
+ }
430
+ };
431
+ const executePublishedSkillsFetchTool = async ({
432
+ args,
433
+ context
434
+ }) => {
435
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
436
+ const requestedSkills = parseRequestedSkillArgs(args);
437
+ const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
438
+ lastInteractiveDirectoryPath = fetchActionDirectoryPath;
439
+ const emitFetchOutcome = async event => {
440
+ await emitActionEventForCurrentSession({
441
+ event,
442
+ directoryPath: fetchActionDirectoryPath
443
+ });
444
+ };
445
+ let publishedSkillsResult = await loadPublishedSkillCatalog({
446
+ directory: requestedDirectory,
447
+ useCache: !args.refresh,
448
+ signal: context.abort
449
+ });
450
+ if (publishedSkillsResult.fetchResult.ok) {
451
+ await scheduleInteractivePresenceStart();
452
+ }
453
+ if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
454
+ try {
455
+ await startLoginCompletion('fetch').then(async authState => {
456
+ await schedulePresenceStart(authState);
457
+ });
458
+ publishedSkillsResult = await loadPublishedSkillCatalog({
459
+ directory: requestedDirectory,
460
+ useCache: false,
461
+ signal: context.abort
462
+ });
463
+ if (publishedSkillsResult.fetchResult.ok) {
464
+ await scheduleInteractivePresenceStart();
465
+ }
466
+ } catch {
467
+ // Return the original fetch failure with the latest login bootstrap snapshot attached.
468
+ }
469
+ }
470
+ if (!publishedSkillsResult.fetchResult.ok) {
471
+ await emitFetchOutcome('FETCH_FAILED');
472
+ return toFetchFailureOutput({
473
+ worktree: input.worktree,
474
+ config,
475
+ publishedSkillsResult,
476
+ loginBootstrapSnapshot: loginBootstrap.snapshot
477
+ });
478
+ }
479
+ const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
480
+ if (!filteredPublishedSkillsResult.fetchResult.ok) {
481
+ await emitFetchOutcome('FETCH_FAILED');
482
+ return toFetchFailureOutput({
483
+ worktree: input.worktree,
484
+ config,
485
+ publishedSkillsResult: filteredPublishedSkillsResult,
486
+ loginBootstrapSnapshot: loginBootstrap.snapshot
487
+ });
488
+ }
489
+ const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
490
+ const isSingleRequest = requestedSkills.length === 1;
491
+ if (requestedSkills.length === 0) {
492
+ const authState = await resolveStoredAuthState(input.worktree, config);
493
+ const catalog = {
494
+ ...toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload),
495
+ availableTools: resolveAvailableTools(authState?.role ?? null)
496
+ };
497
+ context.metadata({
498
+ title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount} active`,
499
+ metadata: {
500
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
501
+ status: 'ready',
502
+ publishedSkillCount: catalog.publishedSkillCount.toString(),
503
+ globalAssignmentCount: catalog.assignmentCounts.global.toString(),
504
+ projectAssignmentCount: catalog.assignmentCounts.project.toString(),
505
+ userAssignmentCount: catalog.assignmentCounts.user.toString(),
506
+ ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
507
+ }
508
+ });
509
+ await emitFetchOutcome('FETCH_SUCCESS');
510
+ return {
511
+ output: JSON.stringify({
512
+ ...catalog,
513
+ status: 'ready',
514
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
515
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
516
+ ignoredPublishedSkills: {
517
+ scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
518
+ count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
519
+ },
520
+ fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
521
+ source: filteredPublishedSkillsResult.fetchResult.source,
522
+ cacheTtlMs: CACHE_TTL_MS,
523
+ 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.'
524
+ }, null, 2),
525
+ metadata: {
526
+ status: 'ready',
527
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
528
+ source: filteredPublishedSkillsResult.fetchResult.source,
529
+ publishedSkillCount: catalog.publishedSkillCount.toString(),
530
+ globalAssignmentCount: catalog.assignmentCounts.global.toString(),
531
+ projectAssignmentCount: catalog.assignmentCounts.project.toString(),
532
+ userAssignmentCount: catalog.assignmentCounts.user.toString(),
533
+ ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
534
+ }
535
+ };
536
+ }
537
+ if (selection.selectedItems.length === 0 && isSingleRequest) {
538
+ await emitFetchOutcome('FETCH_FAILED');
539
+ return {
540
+ output: JSON.stringify({
541
+ pluginId: PLUGIN_ID,
542
+ runtimeMode: 'tool_fetch_only',
543
+ status: 'not_found',
544
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
545
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
546
+ requestedSkill: requestedSkills[0],
547
+ availableSkills: filteredPublishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary),
548
+ ignoredPublishedSkills: {
549
+ scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
550
+ count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
551
+ }
552
+ }, null, 2),
553
+ metadata: {
554
+ status: 'not_found',
555
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution)
556
+ }
557
+ };
558
+ }
559
+ let skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
560
+ workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
561
+ item,
562
+ signal: context.abort,
563
+ useCache: !args.refresh,
564
+ purpose: 'TOOL_FETCH'
565
+ })));
566
+ if (skillDetailResults.some(result => !result.ok && result.status === 'missing_auth')) {
567
+ try {
568
+ await startLoginCompletion('fetch').then(async authState => {
569
+ await schedulePresenceStart(authState);
570
+ });
571
+ skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
572
+ workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
573
+ item,
574
+ signal: context.abort,
575
+ useCache: false,
576
+ purpose: 'TOOL_FETCH'
577
+ })));
578
+ } catch {
579
+ // Return the original detail failure after the login bootstrap attempt updates snapshot state.
580
+ }
581
+ }
582
+ const failedSkillDetail = skillDetailResults.find(result => !result.ok);
583
+ if (failedSkillDetail && !failedSkillDetail.ok) {
584
+ await emitFetchOutcome('FETCH_FAILED');
585
+ return failedSkillDetail;
586
+ }
587
+ const skillDetails = skillDetailResults.map(result => {
588
+ if (!result.ok) {
589
+ throw new Error('Published skill detail result unexpectedly missing after success guard.');
590
+ }
591
+ return result.detail;
592
+ });
593
+ if (isSingleRequest && skillDetails[0]) {
594
+ const detail = skillDetails[0];
595
+ context.metadata({
596
+ title: `opencode-wizard published skill: ${detail.artifactName || detail.skillName}`,
597
+ metadata: {
598
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
599
+ skillSlug: detail.skillSlug,
600
+ version: detail.version
601
+ }
602
+ });
603
+ await emitFetchOutcome('FETCH_SUCCESS');
604
+ return {
605
+ output: JSON.stringify({
606
+ pluginId: PLUGIN_ID,
607
+ runtimeMode: 'tool_fetch_only',
608
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
609
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
610
+ workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
611
+ fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
612
+ source: filteredPublishedSkillsResult.fetchResult.source,
613
+ skill: detail
614
+ }, null, 2),
615
+ metadata: {
616
+ status: 'ready',
617
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
618
+ source: filteredPublishedSkillsResult.fetchResult.source,
619
+ skillSlug: detail.skillSlug
620
+ }
621
+ };
622
+ }
623
+ context.metadata({
624
+ title: `opencode-wizard published skills fetch: ${skillDetails.length}`,
625
+ metadata: {
626
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
627
+ requestedCount: requestedSkills.length.toString(),
628
+ matchedCount: skillDetails.length.toString()
629
+ }
630
+ });
631
+ await emitFetchOutcome('FETCH_SUCCESS');
632
+ return {
633
+ output: JSON.stringify({
634
+ pluginId: PLUGIN_ID,
635
+ runtimeMode: 'tool_fetch_only',
636
+ requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
637
+ workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
638
+ workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
639
+ fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
640
+ source: filteredPublishedSkillsResult.fetchResult.source,
641
+ requestedSkills,
642
+ missingSkills: selection.missingIdentifiers,
643
+ skills: skillDetails
644
+ }, null, 2),
645
+ metadata: {
646
+ status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
647
+ ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
648
+ source: filteredPublishedSkillsResult.fetchResult.source,
649
+ matchedCount: skillDetails.length.toString()
650
+ }
651
+ };
652
+ };
653
+ const executeWizardArtifactCatalogTool = async ({
654
+ args,
655
+ context
656
+ }) => {
657
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
658
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
659
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
660
+ if (!artifactKind) {
661
+ return buildUnsupportedWizardArtifactOutput({
662
+ artifactKind: args.artifactKind ?? '',
663
+ directoryPath
664
+ });
665
+ }
666
+ if (artifactKind === 'DESIGN_DOC') {
667
+ const workspaceResolution = await resolveWorkspace({
668
+ config,
669
+ directory: requestedDirectory
670
+ });
671
+ const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, clearPublishedSkillState);
672
+ if (!fetchResult.ok) {
673
+ return {
674
+ output: JSON.stringify({
675
+ pluginId: PLUGIN_ID,
676
+ runtimeMode: 'tool_fetch_only',
677
+ status: fetchResult.status,
678
+ artifactKind,
679
+ requestedDirectoryPath: directoryPath,
680
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
681
+ message: fetchResult.message,
682
+ fetchedAt: fetchResult.fetchedAt,
683
+ source: fetchResult.source
684
+ }, null, 2),
685
+ metadata: {
686
+ status: fetchResult.status,
687
+ artifactKind,
688
+ directoryPath,
689
+ source: fetchResult.source
690
+ }
691
+ };
692
+ }
693
+ const authState = await resolveStoredAuthState(input.worktree, config);
694
+ const catalog = toWizardArtifactCatalog(fetchResult.payload, {
695
+ pluginId: PLUGIN_ID,
696
+ availableTools: resolveAvailableTools(authState?.role ?? null)
697
+ });
698
+ return {
699
+ output: JSON.stringify({
700
+ ...catalog,
701
+ status: 'ready',
702
+ requestedDirectoryPath: directoryPath,
703
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
704
+ fetchedAt: fetchResult.fetchedAt,
705
+ source: fetchResult.source,
706
+ message: 'DESIGN_DOC catalog discovery only. Full DESIGN.md bodies/files require opencode_wizard_artifact_fetch with artifactKind DESIGN_DOC.'
707
+ }, null, 2),
708
+ metadata: {
709
+ status: 'ready',
710
+ artifactKind,
711
+ artifactCount: catalog.artifactCount.toString(),
712
+ ...toWorkspaceResolutionMetadata(workspaceResolution),
713
+ source: fetchResult.source
714
+ }
715
+ };
716
+ }
717
+ const result = await executePublishedSkillsFetchTool({
718
+ args: {
719
+ directory: args.directory,
720
+ refresh: args.refresh
721
+ },
722
+ context
723
+ });
724
+ return withWizardArtifactEnvelope(result, artifactKind);
725
+ };
726
+ const executeWizardArtifactFetchTool = async ({
727
+ args,
728
+ context
729
+ }) => {
730
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
731
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
732
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
733
+ if (!artifactKind) {
734
+ return buildUnsupportedWizardArtifactOutput({
735
+ artifactKind: args.artifactKind ?? '',
736
+ directoryPath
737
+ });
738
+ }
739
+ if (artifactKind === 'DESIGN_DOC') {
740
+ const requestedArtifacts = parseRequestedSkillArgs({
741
+ skill: args.artifact,
742
+ skills: args.artifacts
743
+ });
744
+ const workspaceResolution = await resolveWorkspace({
745
+ config,
746
+ directory: requestedDirectory
747
+ });
748
+ const fetchResult = await fetchWizardArtifactsCatalog(input.worktree, config, workspaceResolution, artifactKind, context.abort, clearPublishedSkillState);
749
+ if (!fetchResult.ok) {
750
+ return {
751
+ output: JSON.stringify({
752
+ pluginId: PLUGIN_ID,
753
+ runtimeMode: 'tool_fetch_only',
754
+ status: fetchResult.status,
755
+ artifactKind,
756
+ requestedDirectoryPath: directoryPath,
757
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
758
+ message: fetchResult.message,
759
+ fetchedAt: fetchResult.fetchedAt,
760
+ source: fetchResult.source
761
+ }, null, 2),
762
+ metadata: {
763
+ status: fetchResult.status,
764
+ artifactKind,
765
+ directoryPath,
766
+ source: fetchResult.source
767
+ }
768
+ };
769
+ }
770
+ const selection = selectWizardArtifacts(fetchResult.payload.artifacts, requestedArtifacts);
771
+ if (requestedArtifacts.length === 0) {
772
+ const authState = await resolveStoredAuthState(input.worktree, config);
773
+ const catalog = toWizardArtifactCatalog(fetchResult.payload, {
774
+ pluginId: PLUGIN_ID,
775
+ availableTools: resolveAvailableTools(authState?.role ?? null)
776
+ });
777
+ return {
778
+ output: JSON.stringify({
779
+ ...catalog,
780
+ status: 'ready',
781
+ requestedDirectoryPath: directoryPath,
782
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
783
+ fetchedAt: fetchResult.fetchedAt,
784
+ source: fetchResult.source,
785
+ message: 'Provide artifact or artifacts to fetch DESIGN.md body/files.'
786
+ }, null, 2),
787
+ metadata: {
788
+ status: 'ready',
789
+ artifactKind,
790
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
791
+ }
792
+ };
793
+ }
794
+ if (selection.selectedItems.length === 0 && requestedArtifacts.length === 1) {
795
+ return {
796
+ output: JSON.stringify({
797
+ pluginId: PLUGIN_ID,
798
+ runtimeMode: 'tool_fetch_only',
799
+ status: 'not_found',
800
+ artifactKind,
801
+ requestedArtifact: requestedArtifacts[0],
802
+ availableArtifacts: fetchResult.payload.artifacts.map(toWizardArtifactSummary),
803
+ requestedDirectoryPath: directoryPath,
804
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution)
805
+ }, null, 2),
806
+ metadata: {
807
+ status: 'not_found',
808
+ artifactKind,
809
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
810
+ }
811
+ };
812
+ }
813
+ const detailResults = await Promise.all(selection.selectedItems.map(item => fetchWizardArtifactDetail({
814
+ worktree: input.worktree,
815
+ config,
816
+ resolution: workspaceResolution,
817
+ artifactKind,
818
+ artifactVersionId: item.artifactVersion.id,
819
+ signal: context.abort,
820
+ onAuthStateChanged: clearPublishedSkillState,
821
+ purpose: 'TOOL_FETCH'
822
+ })));
823
+ const failedDetail = detailResults.find(result => !result.ok);
824
+ if (failedDetail && !failedDetail.ok) {
825
+ return {
826
+ output: JSON.stringify({
827
+ pluginId: PLUGIN_ID,
828
+ runtimeMode: 'tool_fetch_only',
829
+ status: failedDetail.result.status,
830
+ artifactKind,
831
+ requestedDirectoryPath: directoryPath,
832
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
833
+ message: failedDetail.result.message,
834
+ fetchedAt: failedDetail.result.fetchedAt,
835
+ source: failedDetail.result.source
836
+ }, null, 2),
837
+ metadata: {
838
+ status: failedDetail.result.status,
839
+ artifactKind,
840
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
841
+ }
842
+ };
843
+ }
844
+ const details = detailResults.map((result, index) => {
845
+ if (!result.ok) throw new Error('Wizard artifact detail result unexpectedly missing after success guard.');
846
+ return toWizardArtifactDetail({
847
+ ...selection.selectedItems[index],
848
+ artifactVersion: result.artifact
849
+ });
850
+ });
851
+ return {
852
+ output: JSON.stringify({
853
+ pluginId: PLUGIN_ID,
854
+ runtimeMode: 'tool_fetch_only',
855
+ artifactKind,
856
+ requestedDirectoryPath: directoryPath,
857
+ workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
858
+ workspace: fetchResult.payload.workspace,
859
+ fetchedAt: fetchResult.fetchedAt,
860
+ source: fetchResult.source,
861
+ requestedArtifacts,
862
+ missingArtifacts: selection.missingIdentifiers,
863
+ artifacts: details
864
+ }, null, 2),
865
+ metadata: {
866
+ status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
867
+ artifactKind,
868
+ matchedCount: details.length.toString(),
869
+ ...toWorkspaceResolutionMetadata(workspaceResolution)
870
+ }
871
+ };
872
+ }
873
+ const result = await executePublishedSkillsFetchTool({
874
+ args: {
875
+ skill: args.artifact,
876
+ skills: args.artifacts,
877
+ directory: args.directory,
878
+ refresh: args.refresh
879
+ },
880
+ context
881
+ });
882
+ return withWizardArtifactEnvelope(result, artifactKind);
883
+ };
884
+ const executeStatusTool = async ({
885
+ args,
886
+ context
887
+ }) => {
888
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
889
+ let snapshot = await resolvePluginStatusSnapshot({
890
+ worktree: input.worktree,
891
+ directory: requestedDirectory,
892
+ signal: context.abort
893
+ });
894
+ if (snapshot.status === 'missing_auth') {
895
+ try {
896
+ await startLoginCompletion('status').then(async authState => {
897
+ await schedulePresenceStart(authState);
898
+ });
899
+ snapshot = await resolvePluginStatusSnapshot({
900
+ worktree: input.worktree,
901
+ directory: requestedDirectory,
902
+ signal: context.abort
903
+ });
904
+ } catch {
905
+ // Keep returning the safe missing-auth snapshot when interactive login is cancelled or fails.
906
+ }
907
+ }
908
+ if (snapshot.status === 'ready') {
909
+ await scheduleInteractivePresenceStart();
910
+ }
911
+ const metadata = toPluginStatusMetadata(snapshot);
912
+ context.metadata({
913
+ title: `opencode-wizard status: ${snapshot.status} / auth ${snapshot.authState.status}`,
914
+ metadata
915
+ });
916
+ return {
917
+ output: JSON.stringify(toAiFacingPluginStatusSnapshot(snapshot), null, 2),
918
+ metadata
919
+ };
920
+ };
921
+ const executePublishedSkillPreferenceTool = async ({
922
+ args,
923
+ context
924
+ }) => {
925
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
926
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
927
+ lastInteractiveDirectoryPath = directoryPath;
928
+ const requestedSkill = typeof args.skill === 'string' ? args.skill.trim() : '';
929
+ const emitPreferenceOutcome = async event => {
930
+ await emitActionEventForCurrentSession({
931
+ event,
932
+ directoryPath
933
+ });
934
+ };
935
+ try {
936
+ if (!requestedSkill) {
937
+ throw new Error('Published skill preference tool requires a non-empty skill slug, artifact name, or skill name.');
938
+ }
939
+ if (typeof args.action !== 'string') {
940
+ throw new Error('Published skill preference tool requires an action: install, uninstall, ignore, or unignore.');
941
+ }
942
+ const action = toPublishedSkillPreferenceAction(args.action);
943
+ const catalogResult = await loadPublishedSkillCatalog({
944
+ directory: requestedDirectory,
945
+ useCache: true,
946
+ signal: context.abort
947
+ });
948
+ if (!catalogResult.fetchResult.ok) {
949
+ await emitPreferenceOutcome('PREFERENCE_FAILED');
950
+ return toFetchFailureOutput({
951
+ worktree: input.worktree,
952
+ config,
953
+ publishedSkillsResult: catalogResult,
954
+ loginBootstrapSnapshot: loginBootstrap.snapshot
955
+ });
956
+ }
957
+ const selectableCatalogSkills = catalogResult.fetchResult.payload.catalogSkills.map(item => ({
958
+ ...item,
959
+ assignmentSource: 'CATALOG',
960
+ assignmentType: 'PATH',
961
+ scopePath: '',
962
+ includeChildren: true
963
+ }));
964
+ const preferenceSelection = selectPublishedSkills({
965
+ ...catalogResult.fetchResult.payload,
966
+ skills: [...catalogResult.fetchResult.payload.skills, ...selectableCatalogSkills, ...catalogResult.fetchResult.payload.userPreferences.ignoredSkills]
967
+ }, [requestedSkill]);
968
+ const matchedSkill = preferenceSelection.selectedItems[0];
969
+ if (!matchedSkill) {
970
+ throw new Error(`Published skill preference target was not found for identifier: ${requestedSkill}.`);
971
+ }
972
+ const skillSlug = matchedSkill.skill.slug;
973
+ const preferenceState = action === 'ignore' || action === 'unignore' ? await setPublishedSkillIgnored({
974
+ worktree: input.worktree,
975
+ directory: requestedDirectory,
976
+ skillSlug,
977
+ ignored: action === 'ignore',
978
+ preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project')
979
+ }) : await setPublishedSkillInstalled({
980
+ worktree: input.worktree,
981
+ directory: requestedDirectory,
982
+ skillSlug,
983
+ installed: action === 'install',
984
+ preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project')
985
+ });
986
+ await scheduleInteractivePresenceStart();
987
+ await emitPreferenceOutcome('PREFERENCE_SUCCESS');
988
+ const metadata = {
989
+ status: 'updated',
990
+ skillSlug,
991
+ action,
992
+ directoryPath,
993
+ ignoredSkillCount: preferenceState.ignoredSkillSlugs.length.toString()
994
+ };
995
+ context.metadata({
996
+ title: `opencode-wizard published skill preference: ${action} ${skillSlug}`,
997
+ metadata
998
+ });
999
+ return {
1000
+ output: JSON.stringify({
1001
+ pluginId: PLUGIN_ID,
1002
+ status: 'updated',
1003
+ requestedIdentifier: requestedSkill,
1004
+ skillSlug,
1005
+ action,
1006
+ requestedDirectoryPath: directoryPath,
1007
+ preferenceState,
1008
+ message: 'Published skill preference updated through the shared server-backed API; TUI views will reflect this after refresh.'
1009
+ }, null, 2),
1010
+ metadata
1011
+ };
1012
+ } catch (error) {
1013
+ await emitPreferenceOutcome('PREFERENCE_FAILED');
1014
+ const message = error instanceof Error ? error.message : 'Unknown published skill preference failure.';
1015
+ const metadata = {
1016
+ status: 'preference_failed',
1017
+ directoryPath
1018
+ };
1019
+ context.metadata({
1020
+ title: 'opencode-wizard published skill preference failed',
1021
+ metadata
1022
+ });
1023
+ return {
1024
+ output: JSON.stringify({
1025
+ pluginId: PLUGIN_ID,
1026
+ status: 'preference_failed',
1027
+ requestedIdentifier: requestedSkill,
1028
+ requestedDirectoryPath: directoryPath,
1029
+ message,
1030
+ guidance: 'The preference tool remains available. Check opencode_wizard_status for safe auth/catalog state, rerun fetch/status to bootstrap auth, and retry after catalog status is ready.'
1031
+ }, null, 2),
1032
+ metadata
1033
+ };
1034
+ }
1035
+ };
1036
+ const executeWizardArtifactPreferenceTool = async ({
1037
+ args,
1038
+ context
1039
+ }) => {
1040
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
1041
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
1042
+ const artifactKind = toWizardArtifactKind(args.artifactKind);
1043
+ if (!artifactKind) {
1044
+ return buildUnsupportedWizardArtifactOutput({
1045
+ artifactKind: args.artifactKind ?? '',
1046
+ directoryPath
1047
+ });
1048
+ }
1049
+ if (artifactKind === 'DESIGN_DOC') {
1050
+ try {
1051
+ const action = toPublishedSkillPreferenceAction(args.action);
1052
+ const workspaceResolution = await resolveWorkspace({
1053
+ config,
1054
+ directory: requestedDirectory
1055
+ });
1056
+ const response = await fetchPublishedSkillsGraphQl({
1057
+ worktree: input.worktree,
1058
+ config,
1059
+ query: SET_WIZARD_ARTIFACT_PREFERENCE_MUTATION,
1060
+ variables: {
1061
+ input: {
1062
+ workspaceSlug: workspaceResolution.workspaceSlug,
1063
+ repositoryUrl: workspaceResolution.repositoryUrl,
1064
+ directoryPath: workspaceResolution.directoryPath,
1065
+ artifactKind,
1066
+ artifactSlug: args.artifact.trim(),
1067
+ preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project') === 'global' ? 'GLOBAL' : 'WORKSPACE',
1068
+ installed: action === 'install' ? true : action === 'uninstall' ? false : undefined,
1069
+ ignored: action === 'ignore' ? true : action === 'unignore' ? false : undefined
1070
+ }
1071
+ },
1072
+ signal: context.abort,
1073
+ onAuthStateChanged: clearPublishedSkillState
1074
+ });
1075
+ if (!response.ok) {
1076
+ return {
1077
+ output: JSON.stringify({
1078
+ pluginId: PLUGIN_ID,
1079
+ status: response.result.status,
1080
+ artifactKind,
1081
+ requestedIdentifier: args.artifact,
1082
+ requestedDirectoryPath: directoryPath,
1083
+ message: response.result.message,
1084
+ fetchedAt: response.result.fetchedAt,
1085
+ source: response.result.source
1086
+ }, null, 2),
1087
+ metadata: {
1088
+ status: response.result.status,
1089
+ artifactKind,
1090
+ directoryPath
1091
+ }
1092
+ };
1093
+ }
1094
+ return {
1095
+ output: JSON.stringify({
1096
+ pluginId: PLUGIN_ID,
1097
+ status: 'updated',
1098
+ artifactKind,
1099
+ requestedIdentifier: args.artifact,
1100
+ action,
1101
+ requestedDirectoryPath: directoryPath,
1102
+ preferenceState: response.data.setWizardArtifactPreference,
1103
+ message: 'Wizard artifact preference updated through the generic backend API.'
1104
+ }, null, 2),
1105
+ metadata: {
1106
+ status: 'updated',
1107
+ artifactKind,
1108
+ directoryPath,
1109
+ action
1110
+ }
1111
+ };
1112
+ } catch (error) {
1113
+ return {
1114
+ output: JSON.stringify({
1115
+ pluginId: PLUGIN_ID,
1116
+ status: 'preference_failed',
1117
+ artifactKind,
1118
+ requestedIdentifier: args.artifact,
1119
+ requestedDirectoryPath: directoryPath,
1120
+ message: error instanceof Error ? error.message : 'Unknown wizard artifact preference failure.'
1121
+ }, null, 2),
1122
+ metadata: {
1123
+ status: 'preference_failed',
1124
+ artifactKind,
1125
+ directoryPath
1126
+ }
1127
+ };
1128
+ }
1129
+ }
1130
+ const result = await executePublishedSkillPreferenceTool({
1131
+ args: {
1132
+ skill: args.artifact,
1133
+ action: args.action,
1134
+ preferenceScope: args.preferenceScope,
1135
+ directory: args.directory
1136
+ },
1137
+ context
1138
+ });
1139
+ return withWizardArtifactEnvelope(result, artifactKind);
1140
+ };
1141
+ const executeEditorPublishSkillTool = async ({
1142
+ args,
1143
+ context
1144
+ }) => {
1145
+ const requestedSkillSlug = args.skillSlug.trim();
1146
+ if (!requestedSkillSlug) {
1147
+ throw new Error('Editor publish requires a non-empty skill slug.');
1148
+ }
1149
+ const authState = await resolveStoredAuthState(input.worktree, config);
1150
+ if (!authState || authState.role !== 'EDITOR') {
1151
+ return {
1152
+ output: JSON.stringify({
1153
+ pluginId: PLUGIN_ID,
1154
+ status: 'forbidden',
1155
+ skillSlug: requestedSkillSlug,
1156
+ message: 'This tool requires EDITOR role. Your current session does not have the required editor permission.'
1157
+ }, null, 2),
1158
+ metadata: {
1159
+ status: 'forbidden',
1160
+ role: authState?.role ?? 'none'
1161
+ }
1162
+ };
1163
+ }
1164
+ const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
1165
+ const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
1166
+ lastInteractiveDirectoryPath = directoryPath;
1167
+ const workspaceResolution = await resolveWorkspace({
1168
+ config,
1169
+ directory: requestedDirectory
1170
+ });
1171
+ const repositoryRoot = workspaceResolution.repositoryRoot;
1172
+ const skillFilePath = path.resolve(repositoryRoot, config.rootSkillSeedPath, requestedSkillSlug, 'SKILL.md');
1173
+ let markdownContent;
1174
+ try {
1175
+ markdownContent = await fs.readFile(skillFilePath, 'utf8');
1176
+ } catch (error) {
1177
+ const message = error instanceof Error ? error.message : 'Unknown error';
1178
+ return {
1179
+ output: JSON.stringify({
1180
+ pluginId: PLUGIN_ID,
1181
+ status: 'file_not_found',
1182
+ skillSlug: requestedSkillSlug,
1183
+ skillFilePath,
1184
+ message: `Skill markdown file not found at ${skillFilePath}: ${message}`
1185
+ }, null, 2),
1186
+ metadata: {
1187
+ status: 'file_not_found',
1188
+ skillSlug: requestedSkillSlug,
1189
+ directoryPath
1190
+ }
1191
+ };
1192
+ }
1193
+ const response = await fetchPublishedSkillsGraphQl({
1194
+ worktree: input.worktree,
1195
+ config,
1196
+ query: CREATE_OR_UPDATE_SKILL_FROM_MARKDOWN_MUTATION,
1197
+ variables: {
1198
+ markdownContent
1199
+ },
1200
+ signal: context.abort
1201
+ });
1202
+ if (!response.ok) {
1203
+ return {
1204
+ output: JSON.stringify({
1205
+ pluginId: PLUGIN_ID,
1206
+ status: 'request_failed',
1207
+ skillSlug: requestedSkillSlug,
1208
+ message: response.result.message
1209
+ }, null, 2),
1210
+ metadata: {
1211
+ status: 'request_failed',
1212
+ skillSlug: requestedSkillSlug,
1213
+ directoryPath
1214
+ }
1215
+ };
1216
+ }
1217
+ const payload = response.data.createOrUpdateSkillFromMarkdown;
1218
+ await scheduleInteractivePresenceStart();
1219
+ return {
1220
+ output: JSON.stringify({
1221
+ pluginId: PLUGIN_ID,
1222
+ status: payload.success ? 'published' : 'publish_failed',
1223
+ skillSlug: payload.skillSlug,
1224
+ skillVersionId: payload.skillVersionId,
1225
+ errors: payload.errors
1226
+ }, null, 2),
1227
+ metadata: {
1228
+ status: payload.success ? 'published' : 'publish_failed',
1229
+ skillSlug: payload.skillSlug,
1230
+ skillVersionId: payload.skillVersionId ?? '',
1231
+ directoryPath
1232
+ }
1233
+ };
1234
+ };
1235
+ const role = initialAuthState?.role ?? null;
1236
+ const isEditor = role === 'EDITOR';
1237
+ const {
1238
+ sharedTools,
1239
+ editorOnlyTools
1240
+ } = createPublishedSkillToolDefinitions(tool, {
1241
+ fetchPublishedSkills: (args, context) => executePublishedSkillsFetchTool({
1242
+ args,
1243
+ context
1244
+ }),
1245
+ fetchWizardArtifactCatalog: (args, context) => executeWizardArtifactCatalogTool({
1246
+ args,
1247
+ context
1248
+ }),
1249
+ fetchWizardArtifacts: (args, context) => executeWizardArtifactFetchTool({
1250
+ args,
1251
+ context
1252
+ }),
1253
+ updatePublishedSkillPreference: (args, context) => executePublishedSkillPreferenceTool({
1254
+ args,
1255
+ context
1256
+ }),
1257
+ updateWizardArtifactPreference: (args, context) => executeWizardArtifactPreferenceTool({
1258
+ args,
1259
+ context
1260
+ }),
1261
+ getStatus: (args, context) => executeStatusTool({
1262
+ args,
1263
+ context
1264
+ }),
1265
+ publishEditorSkill: (args, context) => executeEditorPublishSkillTool({
1266
+ args,
1267
+ context
1268
+ })
1269
+ });
1270
+ return {
1271
+ tool: {
1272
+ ...sharedTools,
1273
+ ...(isEditor ? editorOnlyTools : {})
1274
+ },
1275
+ 'experimental.chat.system.transform': async (_hookInput, output) => {
1276
+ let publishedSkillsResult = await loadPublishedSkillCatalog({
1277
+ directory: input.directory,
1278
+ useCache: true,
1279
+ signal: AbortSignal.timeout(5_000)
1280
+ });
1281
+ if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
1282
+ try {
1283
+ await startLoginCompletion('status').then(async authState => {
1284
+ await schedulePresenceStart(authState);
1285
+ });
1286
+ publishedSkillsResult = await loadPublishedSkillCatalog({
1287
+ directory: input.directory,
1288
+ useCache: false,
1289
+ signal: AbortSignal.timeout(5_000)
1290
+ });
1291
+ } catch {
1292
+ const loginMessage = loginBootstrap.snapshot.message ? ` Last login status: ${loginBootstrap.snapshot.message}` : '';
1293
+ output.system.push(`opencode-wizard plugin stored auth is missing, expired, or rejected. Startup browser login was started but did not complete successfully.${loginMessage} Use opencode_wizard_status or opencode_wizard_published_skills_fetch to retry authentication when published skills are needed. No tokens are exposed.`);
1294
+ return;
1295
+ }
1296
+ if (!publishedSkillsResult.fetchResult.ok) {
1297
+ output.system.push(`opencode-wizard plugin startup login completed, but published skills are still unavailable: ${publishedSkillsResult.fetchResult.message} No tokens are exposed.`);
1298
+ return;
1299
+ }
1300
+ }
1301
+ if (publishedSkillsResult.fetchResult.ok) {
1302
+ await scheduleInteractivePresenceStart();
1303
+ }
1304
+ const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
1305
+ const systemNote = buildSystemNote(filteredPublishedSkillsResult, config, []);
1306
+ if (!systemNote) return;
1307
+ output.system.push(systemNote);
1308
+ }
1309
+ };
1310
+ };
1311
+ export default {
1312
+ id: PLUGIN_ID,
1313
+ server: OpencodeWizardSkillsPlugin
1314
+ };
1315
+ //# sourceMappingURL=runtime.js.map