@aexol/opencode-wizard 0.3.3 → 0.3.4

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