@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
package/dist/server.js CHANGED
@@ -1,2614 +1,7 @@
1
- import fs from 'node:fs/promises';
2
- import http from 'node:http';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import crypto from 'node:crypto';
6
- import { execFile } from 'node:child_process';
7
- import { promisify } from 'node:util';
8
- import { URL, fileURLToPath } from 'node:url';
9
- import { resolveBackendOriginFromValues } from './config.js';
10
- import { sendOAuthCallbackHtmlResponse } from './oauth-callback-page.js';
11
- import { deleteFileIfExists, readJsonFile, writePrivateJsonFile } from './storage.js';
12
- const execFileAsync = promisify(execFile);
13
- const MODULE_FILE_PATH = fileURLToPath(import.meta.url);
14
- const PACKAGE_ROOT_PATH = path.resolve(path.dirname(MODULE_FILE_PATH), '..');
15
- export const PLUGIN_ID = 'opencode-wizard';
16
- const CACHE_TTL_MS = 30_000;
17
- const WORKSPACE_MAPPING_LIMIT = 100;
18
- const ROOT_SKILL_SEED_PATH = '.opencode/skills';
19
- const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode-wizard.json');
20
- const LEGACY_AUTH_STATE_PATH = 'plugin/opencode-wizard/.generated/auth-state.json';
21
- const OIDC_ISSUER = 'https://login.microsoftonline.com/86f4caf4-0d6f-4682-9a06-ea57f3e4e76c/v2.0';
22
- const OIDC_CLIENT_ID = 'da963901-2375-442b-9e99-14e59f43eda2';
23
- const OIDC_CALLBACK_ORIGIN = 'http://localhost:24953';
24
- const OIDC_CALLBACK_PATH = '/oauth/callback';
25
- const OIDC_CALLBACK_URL = `${OIDC_CALLBACK_ORIGIN}${OIDC_CALLBACK_PATH}`;
26
- const OIDC_SCOPES = ['openid', 'profile', 'email'];
27
- const LOGIN_TIMEOUT_MS = 5 * 60_000;
28
- const SYSTEM_NOTE_SKILL_NAME_LIMIT = 10;
29
- const SYSTEM_NOTE_DETAIL_LIMIT = 3;
30
- const SYSTEM_NOTE_DETAIL_CHAR_LIMIT = 2_400;
31
- const SYSTEM_NOTE_SKILL_DESCRIPTION_LIMIT = 140;
32
- const PRESENCE_EVENT_TIMEOUT_MS = 3_000;
33
- const PRESENCE_EVENT_MAX_ATTEMPTS = 2;
34
- const PRESENCE_EVENT_RETRY_DELAY_MS = 250;
35
- const PRESENCE_SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
36
- const PRESENCE_SIGNAL_EXIT_CODES = {
37
- SIGINT: 130,
38
- SIGTERM: 143,
39
- SIGHUP: 129
40
- };
41
- const createIdleLoginBootstrapSnapshot = () => ({
42
- status: 'idle',
43
- trigger: null,
44
- startedAt: null,
45
- expiresAt: null,
46
- browserUrl: null,
47
- browserOpenError: null,
48
- email: null,
49
- message: null
50
- });
51
- const STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS = 60_000;
52
- const statusPathLoginBootstrap = {
53
- promise: null,
54
- status: 'idle',
55
- message: null,
56
- failedAt: null
57
- };
58
- const importOpencodePluginModule = new Function('specifier', 'return import(specifier)');
59
- export const AVAILABLE_PUBLISHED_SKILL_TOOLS = ['opencode_wizard_published_skills_fetch', 'opencode_wizard_published_skill_preference_set', 'opencode_wizard_status'];
60
- let publishedSkillPreferenceCacheVersion = 0;
61
- export const NATIVE_SKILLS_URL_COMPATIBILITY = {
62
- configKey: 'skills.urls',
63
- deliveryMode: 'public_static_registry',
64
- wizardPrivateDelivery: 'authenticated_scoped_fetch_tool',
65
- authSupport: 'none',
66
- guidance: 'OpenCode skills.urls is for public/static registries and complements opencode-wizard; private workspace-scoped skills stay available through the authenticated fetch tool only.'
67
- };
68
- const PUBLISHED_SKILLS_CATALOG_QUERY = `
69
- query PluginPublishedSkills($input: PublishedSkillsDeliveryInput!) {
70
- pluginPublishedSkills(input: $input) {
71
- workspace {
72
- id
73
- slug
74
- name
75
- repositoryUrl
76
- defaultBranch
77
- status
78
- }
79
- directoryPath
80
- skills {
81
- assignmentSource
82
- assignmentType
83
- scopePath
84
- includeChildren
85
- skill {
86
- id
87
- slug
88
- name
89
- summary
90
- whenToUse
91
- status
92
- installPolicy
93
- tags {
94
- id
95
- slug
96
- label
97
- description
98
- facet {
99
- id
100
- slug
101
- label
102
- description
103
- }
104
- }
105
- }
106
- skillVersion {
107
- id
108
- version
109
- title
110
- summary
111
- status
112
- }
113
- publishedArtifact {
114
- id
115
- frontmatterName
116
- frontmatterDescription
117
- checksum
118
- publishedAt
119
- fileCount
120
- }
121
- }
122
- catalogSkills {
123
- skill {
124
- id
125
- slug
126
- name
127
- summary
128
- whenToUse
129
- status
130
- installPolicy
131
- tags {
132
- id
133
- slug
134
- label
135
- description
136
- facet {
137
- id
138
- slug
139
- label
140
- description
141
- }
142
- }
143
- }
144
- skillVersion {
145
- id
146
- version
147
- title
148
- summary
149
- status
150
- }
151
- publishedArtifact {
152
- id
153
- frontmatterName
154
- frontmatterDescription
155
- checksum
156
- publishedAt
157
- fileCount
158
- }
159
- }
160
- userPreferences {
161
- scopeKey
162
- userKey
163
- ignoredSkills {
164
- assignmentSource
165
- assignmentType
166
- scopePath
167
- includeChildren
168
- skill {
169
- id
170
- slug
171
- name
172
- summary
173
- whenToUse
174
- status
175
- installPolicy
176
- tags {
177
- id
178
- slug
179
- label
180
- description
181
- facet {
182
- id
183
- slug
184
- label
185
- description
186
- }
187
- }
188
- }
189
- skillVersion {
190
- id
191
- version
192
- title
193
- summary
194
- status
195
- }
196
- publishedArtifact {
197
- id
198
- frontmatterName
199
- frontmatterDescription
200
- checksum
201
- publishedAt
202
- fileCount
203
- }
204
- }
205
- }
206
- }
207
- }
208
- `;
209
- const SET_PUBLISHED_SKILL_PREFERENCE_MUTATION = `
210
- mutation SetPublishedSkillPreference($input: SetPublishedSkillPreferenceInput!) {
211
- setPublishedSkillPreference(input: $input) {
212
- scopeKey
213
- userKey
214
- ignoredSkills {
215
- assignmentSource
216
- assignmentType
217
- scopePath
218
- includeChildren
219
- skill {
220
- id
221
- slug
222
- name
223
- summary
224
- whenToUse
225
- status
226
- installPolicy
227
- tags {
228
- id
229
- slug
230
- label
231
- description
232
- facet {
233
- id
234
- slug
235
- label
236
- description
237
- }
238
- }
239
- }
240
- skillVersion {
241
- id
242
- version
243
- title
244
- summary
245
- status
246
- }
247
- publishedArtifact {
248
- id
249
- frontmatterName
250
- frontmatterDescription
251
- checksum
252
- publishedAt
253
- fileCount
254
- }
255
- }
256
- }
257
- }
258
- `;
259
- const PUBLISHED_SKILL_DETAIL_QUERY = `
260
- query PluginPublishedSkillVersionArtifact($input: PublishedSkillArtifactDetailInput!) {
261
- pluginPublishedSkillVersionArtifact(input: $input) {
262
- id
263
- frontmatterName
264
- frontmatterDescription
265
- markdownBody
266
- renderedContent
267
- checksum
268
- publishedAt
269
- fileCount
270
- files {
271
- id
272
- relativePath
273
- contentType
274
- content
275
- checksum
276
- size
277
- sortOrder
278
- }
279
- }
280
- }
281
- `;
282
- const parseDotEnvValue = value => {
283
- const trimmedValue = value.trim();
284
- if (trimmedValue.startsWith('"') && trimmedValue.endsWith('"') || trimmedValue.startsWith("'") && trimmedValue.endsWith("'")) {
285
- return trimmedValue.slice(1, -1);
286
- }
287
- return trimmedValue;
288
- };
289
- const parseDotEnv = raw => {
290
- const values = new Map();
291
- for (const line of raw.split(/\r?\n/u)) {
292
- const trimmedLine = line.trim();
293
- if (!trimmedLine || trimmedLine.startsWith('#')) continue;
294
- const separatorIndex = trimmedLine.indexOf('=');
295
- if (separatorIndex <= 0) continue;
296
- const key = trimmedLine.slice(0, separatorIndex).trim();
297
- if (!key) continue;
298
- const rawValue = trimmedLine.slice(separatorIndex + 1);
299
- values.set(key, parseDotEnvValue(rawValue));
300
- }
301
- return values;
302
- };
303
- const findUpwardFile = async (startDirectory, fileName) => {
304
- let currentDirectory = normalizeAbsolutePath(startDirectory);
305
- while (true) {
306
- const candidatePath = path.join(currentDirectory, fileName);
307
- try {
308
- await fs.access(candidatePath);
309
- return candidatePath;
310
- } catch {
311
- const parentDirectory = path.dirname(currentDirectory);
312
- if (parentDirectory === currentDirectory) return null;
313
- currentDirectory = parentDirectory;
314
- }
315
- }
316
- };
317
- const readLocalEnvValues = async startDirectory => {
318
- const envPath = await findUpwardFile(startDirectory, '.env');
319
- if (!envPath) return new Map();
320
- try {
321
- const raw = await fs.readFile(envPath, 'utf8');
322
- return parseDotEnv(raw);
323
- } catch {
324
- return new Map();
325
- }
326
- };
327
- const resolveBackendOrigin = async worktree => {
328
- const envValues = await readLocalEnvValues(worktree);
329
- return resolveBackendOriginFromValues({
330
- environmentBackendOrigin: process.env.OPENCODE_WIZARD_BACKEND_ORIGIN,
331
- localBackendOrigin: envValues.get('OPENCODE_WIZARD_BACKEND_ORIGIN')
332
- });
333
- };
334
- const readConfiguredWorkspaceSlug = () => {
335
- const configuredWorkspaceSlug = process.env.OPENCODE_WIZARD_SKILLS_WORKSPACE_SLUG?.trim();
336
- if (!configuredWorkspaceSlug) return null;
337
- return toWorkspaceSlug(configuredWorkspaceSlug);
338
- };
339
- const toWorkspaceSlug = value => {
340
- const normalized = value.trim().toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '');
341
- if (normalized) return normalized;
342
- return 'workspace';
343
- };
344
- const resolveFallbackWorkspaceSlug = worktree => {
345
- const configuredWorkspaceSlug = readConfiguredWorkspaceSlug();
346
- if (configuredWorkspaceSlug) return configuredWorkspaceSlug;
347
- return toWorkspaceSlug(path.basename(path.resolve(worktree)));
348
- };
349
- export const resolveConfig = async worktree => {
350
- const backendOrigin = await resolveBackendOrigin(worktree);
351
- return {
352
- backendOrigin,
353
- graphqlUrl: `${backendOrigin}/graphql`,
354
- authSessionUrl: `${backendOrigin}/api/opencode-plugin/oauth/session`,
355
- presenceUrl: `${backendOrigin}/api/opencode-plugin/presence`,
356
- actionsUrl: `${backendOrigin}/api/opencode-plugin/actions`,
357
- configuredWorkspaceSlug: readConfiguredWorkspaceSlug(),
358
- fallbackWorkspaceSlug: resolveFallbackWorkspaceSlug(worktree),
359
- rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
360
- authStatePath: GLOBAL_CONFIG_PATH
361
- };
362
- };
363
- const normalizeAbsolutePath = value => path.resolve(value);
364
- const normalizeRepositoryPath = (worktree, directory) => {
365
- const absoluteWorktree = normalizeAbsolutePath(worktree);
366
- const absoluteDirectory = normalizeAbsolutePath(directory);
367
- const relativePath = path.relative(absoluteWorktree, absoluteDirectory);
368
- if (!relativePath || relativePath === '') return '.';
369
- if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) return '.';
370
- return relativePath.split(path.sep).join('/');
371
- };
372
- const tryExecGit = async args => {
373
- try {
374
- const {
375
- stdout
376
- } = await execFileAsync('git', args, {
377
- encoding: 'utf8'
378
- });
379
- const normalizedOutput = stdout.trim();
380
- if (normalizedOutput) return normalizedOutput;
381
- return null;
382
- } catch {
383
- return null;
384
- }
385
- };
386
- const resolveGitRoot = async directory => {
387
- const gitRoot = await tryExecGit(['-C', directory, 'rev-parse', '--show-toplevel']);
388
- if (!gitRoot) return null;
389
- return normalizeAbsolutePath(gitRoot);
390
- };
391
- const normalizeGitRemoteUrl = remoteUrl => {
392
- if (!remoteUrl) return null;
393
- const trimmedRemoteUrl = remoteUrl.trim();
394
- if (!trimmedRemoteUrl) return null;
395
- const scpLikeMatch = /^git@([^:]+):(.+)$/u.exec(trimmedRemoteUrl);
396
- if (scpLikeMatch) {
397
- return `ssh://git@${scpLikeMatch[1]}/${scpLikeMatch[2].replace(/^\/+/, '')}`;
398
- }
399
- try {
400
- const parsedUrl = new URL(trimmedRemoteUrl);
401
- return parsedUrl.toString().replace(/\/+$/u, '');
402
- } catch {
403
- return null;
404
- }
405
- };
406
- const resolveGitRemoteOriginUrl = async repositoryRoot => {
407
- const remoteUrl = await tryExecGit(['-C', repositoryRoot, 'remote', 'get-url', 'origin']);
408
- return normalizeGitRemoteUrl(remoteUrl);
409
- };
410
- const resolveWorkspace = async ({
411
- config,
412
- directory
413
- }) => {
414
- const requestedDirectory = normalizeAbsolutePath(directory);
415
- const gitRoot = await resolveGitRoot(requestedDirectory);
416
- const repositoryRoot = gitRoot ?? requestedDirectory;
417
- const repositoryUrl = gitRoot ? await resolveGitRemoteOriginUrl(gitRoot) : null;
418
- const learnedWorkspaceMapping = await findWorkspaceSlugMapping({
419
- configFile: config.authStatePath,
420
- repositoryUrl,
421
- repositoryRoot
422
- });
423
- const fallbackWorkspaceSlug = config.fallbackWorkspaceSlug;
424
- const directoryPath = normalizeRepositoryPath(repositoryRoot, requestedDirectory);
425
- const workspaceSlug = config.configuredWorkspaceSlug ?? learnedWorkspaceMapping?.workspaceSlug ?? fallbackWorkspaceSlug ?? null;
426
- const workspaceSlugSource = config.configuredWorkspaceSlug ? 'configured' : learnedWorkspaceMapping?.workspaceSlug ? 'learned' : fallbackWorkspaceSlug ? 'fallback' : repositoryUrl ? 'repositoryUrl' : 'placeholder';
427
- const workspaceIdentity = workspaceSlug ? `workspaceSlug:${workspaceSlug}` : repositoryUrl ? `repository:${repositoryUrl}` : 'workspace:placeholder';
428
- return {
429
- requestedDirectory,
430
- repositoryRoot,
431
- repositoryUrl,
432
- workspaceSlug,
433
- workspaceSlugSource,
434
- fallbackWorkspaceSlug,
435
- directoryPath,
436
- cacheKey: JSON.stringify([workspaceIdentity, directoryPath])
437
- };
438
- };
439
- const toDeliveryInput = resolution => {
440
- if (resolution.workspaceSlug) {
441
- return {
442
- workspaceSlug: resolution.workspaceSlug,
443
- directoryPath: resolution.directoryPath
444
- };
445
- }
446
- if (resolution.repositoryUrl) {
447
- return {
448
- repositoryUrl: resolution.repositoryUrl,
449
- directoryPath: resolution.directoryPath
450
- };
451
- }
452
- return {
453
- workspaceSlug: 'workspace',
454
- directoryPath: resolution.directoryPath
455
- };
456
- };
457
- const formatSkillLabel = item => {
458
- const artifactName = item.publishedArtifact.frontmatterName.trim();
459
- if (artifactName.length > 0) return artifactName;
460
- return item.skill.name;
461
- };
462
- const toFrontmatterString = value => JSON.stringify(value);
463
- const isRecord = value => {
464
- return typeof value === 'object' && value !== null && !Array.isArray(value);
465
- };
466
- const isValidIsoDateString = value => {
467
- return typeof value === 'string' && Number.isFinite(Date.parse(value));
468
- };
469
- const isAuthState = value => {
470
- if (!isRecord(value)) return false;
471
- return value.pluginId === PLUGIN_ID && typeof value.sessionToken === 'string' && isValidIsoDateString(value.expiresAt) && isValidIsoDateString(value.authenticatedAt) && typeof value.userId === 'string' && typeof value.email === 'string';
472
- };
473
- const readGlobalConfig = async configFile => {
474
- const storedConfig = await readJsonFile(configFile);
475
- if (isRecord(storedConfig)) return storedConfig;
476
- return {};
477
- };
478
- const writeGlobalConfig = async (configFile, config) => {
479
- await writePrivateJsonFile(configFile, config);
480
- };
481
- const withoutLegacyPublishedSkillPreferences = config => {
482
- const {
483
- publishedSkillPreferences,
484
- ignoredPublishedSkills,
485
- ...safeConfig
486
- } = config;
487
- void publishedSkillPreferences;
488
- void ignoredPublishedSkills;
489
- return safeConfig;
490
- };
491
- const hasLegacyPublishedSkillPreferences = config => {
492
- return Object.prototype.hasOwnProperty.call(config, 'publishedSkillPreferences') || Object.prototype.hasOwnProperty.call(config, 'ignoredPublishedSkills');
493
- };
494
- const isStoredWorkspaceSlugMapping = value => {
495
- if (!isRecord(value)) return false;
496
- const {
497
- repositoryUrl,
498
- repositoryRoot,
499
- workspaceSlug,
500
- updatedAt
501
- } = value;
502
- const hasValidRepositoryUrl = repositoryUrl === null || typeof repositoryUrl === 'string';
503
- const hasValidRepositoryRoot = repositoryRoot === null || typeof repositoryRoot === 'string';
504
- return hasValidRepositoryUrl && hasValidRepositoryRoot && typeof workspaceSlug === 'string' && workspaceSlug.trim().length > 0 && isValidIsoDateString(updatedAt);
505
- };
506
- const readWorkspaceSlugMappings = async configFile => {
507
- const storedConfig = await readGlobalConfig(configFile);
508
- const mappings = storedConfig.workspaceSlugMappings;
509
- if (!Array.isArray(mappings)) return [];
510
- return mappings.filter(isStoredWorkspaceSlugMapping).slice(0, WORKSPACE_MAPPING_LIMIT);
511
- };
512
- const writeWorkspaceSlugMappings = async (configFile, nextMappings) => {
513
- const storedConfig = await readGlobalConfig(configFile);
514
- await writeGlobalConfig(configFile, {
515
- ...withoutLegacyPublishedSkillPreferences(storedConfig),
516
- workspaceSlugMappings: nextMappings.slice(0, WORKSPACE_MAPPING_LIMIT)
517
- });
518
- };
519
- const normalizeStoredRepositoryRoot = value => {
520
- if (!value) return null;
521
- return normalizeAbsolutePath(value);
522
- };
523
- const upsertWorkspaceSlugMapping = async ({
524
- configFile,
525
- repositoryUrl,
526
- repositoryRoot,
527
- workspaceSlug
528
- }) => {
529
- const normalizedWorkspaceSlug = toWorkspaceSlug(workspaceSlug);
530
- if (!normalizedWorkspaceSlug) return;
531
- const normalizedRepositoryRoot = normalizeStoredRepositoryRoot(repositoryRoot);
532
- const existingMappings = await readWorkspaceSlugMappings(configFile);
533
- const filteredMappings = existingMappings.filter(mapping => {
534
- if (repositoryUrl && mapping.repositoryUrl === repositoryUrl) return false;
535
- if (normalizedRepositoryRoot && normalizeStoredRepositoryRoot(mapping.repositoryRoot) === normalizedRepositoryRoot) {
536
- return false;
537
- }
538
- return true;
539
- });
540
- await writeWorkspaceSlugMappings(configFile, [{
541
- repositoryUrl,
542
- repositoryRoot: normalizedRepositoryRoot,
543
- workspaceSlug: normalizedWorkspaceSlug,
544
- updatedAt: new Date().toISOString()
545
- }, ...filteredMappings]);
546
- };
547
- const findWorkspaceSlugMapping = async ({
548
- configFile,
549
- repositoryUrl,
550
- repositoryRoot
551
- }) => {
552
- const normalizedRepositoryRoot = normalizeStoredRepositoryRoot(repositoryRoot);
553
- const mappings = await readWorkspaceSlugMappings(configFile);
554
- if (repositoryUrl) {
555
- const repositoryMatch = mappings.find(mapping => mapping.repositoryUrl === repositoryUrl);
556
- if (repositoryMatch) return repositoryMatch;
557
- }
558
- if (normalizedRepositoryRoot) {
559
- const rootMatch = mappings.find(mapping => normalizeStoredRepositoryRoot(mapping.repositoryRoot) === normalizedRepositoryRoot);
560
- if (rootMatch) return rootMatch;
561
- }
562
- return null;
563
- };
564
- const readGlobalAuthState = async configFile => {
565
- const storedConfig = await readGlobalConfig(configFile);
566
- const storedAuthState = storedConfig.auth;
567
- if (storedAuthState === undefined || storedAuthState === null) return null;
568
- if (isAuthState(storedAuthState)) {
569
- if (hasLegacyPublishedSkillPreferences(storedConfig)) {
570
- await writeGlobalConfig(configFile, withoutLegacyPublishedSkillPreferences(storedConfig));
571
- }
572
- return storedAuthState;
573
- }
574
- await writeGlobalConfig(configFile, {
575
- ...withoutLegacyPublishedSkillPreferences(storedConfig),
576
- auth: null
577
- });
578
- return null;
579
- };
580
- const readLegacyAuthState = async authStateFile => {
581
- const storedAuthState = await readJsonFile(authStateFile);
582
- if (storedAuthState === null) return null;
583
- if (isAuthState(storedAuthState)) return storedAuthState;
584
- await deleteFileIfExists(authStateFile);
585
- return null;
586
- };
587
- const writeAuthState = async (configFile, authState) => {
588
- const storedConfig = await readGlobalConfig(configFile);
589
- await writeGlobalConfig(configFile, {
590
- ...withoutLegacyPublishedSkillPreferences(storedConfig),
591
- auth: authState
592
- });
593
- };
594
- const clearAuthState = async configFile => {
595
- const storedConfig = await readGlobalConfig(configFile);
596
- await writeGlobalConfig(configFile, {
597
- ...withoutLegacyPublishedSkillPreferences(storedConfig),
598
- auth: null
599
- });
600
- };
601
- const toIgnoredSkillSlug = value => {
602
- const normalized = value.trim().toLowerCase();
603
- if (!normalized) return null;
604
- return normalized;
605
- };
606
- const toPublishedSkillPreferenceAction = value => {
607
- const normalized = value.trim().toLowerCase();
608
- if (normalized === 'install' || normalized === 'uninstall' || normalized === 'ignore' || normalized === 'unignore') {
609
- return normalized;
610
- }
611
- throw new Error('Published skill preference action must be one of: install, uninstall, ignore, unignore.');
612
- };
613
- const toPublishedSkillPreferenceScope = (value, defaultScope) => {
614
- if (!value) return defaultScope;
615
- const normalized = value.trim().toLowerCase();
616
- if (normalized === 'global' || normalized === 'project') return normalized;
617
- throw new Error('Published skill preferenceScope must be either global or project.');
618
- };
619
- const getPublishedSkillIgnoreScopeKey = (resolution, payload) => {
620
- const workspaceSlug = payload?.workspace?.slug ?? resolution.workspaceSlug ?? resolution.fallbackWorkspaceSlug;
621
- if (workspaceSlug) return `workspace:${toWorkspaceSlug(workspaceSlug)}`;
622
- if (resolution.repositoryUrl) return `repository:${resolution.repositoryUrl}`;
623
- return `path:${toWorkspaceSlug(path.basename(resolution.repositoryRoot))}`;
624
- };
625
- const toStoredUserKey = authState => {
626
- if (authState?.userId) return authState.userId;
627
- if (authState?.email) return authState.email.toLowerCase();
628
- return 'anonymous';
629
- };
630
- const resolvePublishedSkillPreferenceCacheContext = async config => {
631
- const authState = await readGlobalAuthState(config.authStatePath);
632
- return {
633
- userKey: toStoredUserKey(authState),
634
- preferenceVersion: publishedSkillPreferenceCacheVersion
635
- };
636
- };
637
- const getCatalogCacheKey = (workspaceResolution, preferenceContext) => {
638
- return JSON.stringify([workspaceResolution.cacheKey, preferenceContext.userKey, preferenceContext.preferenceVersion]);
639
- };
640
- const toAuthState = session => ({
641
- pluginId: PLUGIN_ID,
642
- sessionToken: session.jwtToken,
643
- expiresAt: session.expiresAt,
644
- authenticatedAt: new Date().toISOString(),
645
- userId: session.user.id,
646
- email: session.user.email
647
- });
648
- const resolveStoredAuthState = async (worktree, config) => {
649
- const authState = await readGlobalAuthState(config.authStatePath);
650
- if (authState && Date.parse(authState.expiresAt) > Date.now()) {
651
- return authState;
652
- }
653
- if (authState) {
654
- await clearAuthState(config.authStatePath);
655
- return null;
656
- }
657
- const legacyAuthStateFile = path.resolve(worktree, LEGACY_AUTH_STATE_PATH);
658
- const legacyAuthState = await readLegacyAuthState(legacyAuthStateFile);
659
- if (!legacyAuthState) return null;
660
- if (Date.parse(legacyAuthState.expiresAt) <= Date.now()) {
661
- await deleteFileIfExists(legacyAuthStateFile);
662
- return null;
663
- }
664
- await writeAuthState(config.authStatePath, legacyAuthState);
665
- await deleteFileIfExists(legacyAuthStateFile);
666
- return legacyAuthState;
667
- };
668
- export const buildSkillMarkdown = item => {
669
- const artifactBody = item.publishedArtifact.markdownBody.trim();
670
- const fallbackBody = item.publishedArtifact.renderedContent.trim();
671
- const body = artifactBody || fallbackBody;
672
- if (body.startsWith('---')) {
673
- return body.endsWith('\n') ? body : `${body}\n`;
674
- }
675
- const name = formatSkillLabel(item);
676
- const description = item.publishedArtifact.frontmatterDescription.trim() || item.skill.summary?.trim() || '';
677
- const frontmatter = ['---', `name: ${toFrontmatterString(name)}`, `description: ${toFrontmatterString(description)}`, '---'].join('\n');
678
- if (!body) {
679
- return `${frontmatter}\n`;
680
- }
681
- return `${frontmatter}\n\n${body.endsWith('\n') ? body : `${body}\n`}`;
682
- };
683
- const getSkillIdentifiers = item => {
684
- const candidates = [item.skill.slug, item.publishedArtifact.frontmatterName, item.skill.name];
685
- const seen = new Set();
686
- return candidates.reduce((all, candidate) => {
687
- const normalized = candidate.trim();
688
- if (!normalized) return all;
689
- const cacheKey = normalized.toLowerCase();
690
- if (seen.has(cacheKey)) return all;
691
- seen.add(cacheKey);
692
- all.push(normalized);
693
- return all;
694
- }, []);
695
- };
696
- const toPublishedSkillFacetSummary = facet => ({
697
- slug: facet.slug,
698
- label: facet.label,
699
- description: facet.description ?? null
700
- });
701
- const toPublishedSkillTagSummary = tag => ({
702
- slug: tag.slug,
703
- label: tag.label,
704
- description: tag.description ?? null,
705
- facet: tag.facet ? toPublishedSkillFacetSummary(tag.facet) : null
706
- });
707
- const getPublishedSkillFacets = items => {
708
- const facetsBySlug = new Map();
709
- for (const item of items) {
710
- for (const tag of item.skill.tags) {
711
- if (!tag.facet) continue;
712
- if (facetsBySlug.has(tag.facet.slug)) continue;
713
- facetsBySlug.set(tag.facet.slug, toPublishedSkillFacetSummary(tag.facet));
714
- }
715
- }
716
- return [...facetsBySlug.values()].sort((left, right) => left.slug.localeCompare(right.slug));
717
- };
718
- const isUserPublishedSkillAssignment = assignmentSource => assignmentSource === 'USER' || assignmentSource === 'USER_GLOBAL' || assignmentSource === 'USER_WORKSPACE';
719
- const getPublishedSkillAssignmentLabel = assignmentSource => {
720
- if (assignmentSource === 'GLOBAL') return 'GLOBAL SCOPE assignment';
721
- if (assignmentSource === 'WORKSPACE') return 'PROJECT SCOPE assignment';
722
- if (assignmentSource === 'USER_GLOBAL') return 'USER SCOPE preference (global target)';
723
- if (assignmentSource === 'USER_WORKSPACE') return 'USER SCOPE preference (project target)';
724
- if (assignmentSource === 'USER') return 'USER SCOPE assignment';
725
- return `${assignmentSource.toUpperCase().replace(/_/gu, ' ')} assignment`;
726
- };
727
- const getPublishedSkillAssignmentCounts = items => items.reduce((counts, item) => {
728
- if (isUserPublishedSkillAssignment(item.assignmentSource)) {
729
- return {
730
- ...counts,
731
- user: counts.user + 1
732
- };
733
- }
734
- if (item.assignmentSource === 'GLOBAL') {
735
- return {
736
- ...counts,
737
- global: counts.global + 1
738
- };
739
- }
740
- if (item.assignmentSource === 'WORKSPACE') {
741
- return {
742
- ...counts,
743
- project: counts.project + 1
744
- };
745
- }
746
- return {
747
- ...counts,
748
- other: counts.other + 1
749
- };
750
- }, {
751
- global: 0,
752
- project: 0,
753
- user: 0,
754
- other: 0
755
- });
756
- const getSkillContextKind = item => {
757
- if (item.assignmentSource === 'GLOBAL' || item.assignmentSource === 'USER_GLOBAL') return 'global';
758
- return 'project';
759
- };
760
- const getSkillPolicyLabel = (policy, contextKind, assignmentSource) => {
761
- if (isUserPublishedSkillAssignment(assignmentSource) && policy === 'GLOBAL_CONTEXT') {
762
- return 'GLOBAL_CONTEXT · active USER SCOPE context';
763
- }
764
- if (isUserPublishedSkillAssignment(assignmentSource)) return 'PROJECT_INSTALLABLE · active USER SCOPE preference';
765
- if (policy === 'GLOBAL_CONTEXT') return 'GLOBAL_CONTEXT · active context only, not project-installable';
766
- if (contextKind === 'installable') return 'PROJECT_INSTALLABLE · available to install';
767
- if (contextKind === 'global') return 'PROJECT_INSTALLABLE · active GLOBAL SCOPE assignment';
768
- return 'PROJECT_INSTALLABLE · active PROJECT SCOPE assignment';
769
- };
770
- const toPublishedSkillSummary = item => {
771
- const contextKind = getSkillContextKind(item);
772
- return {
773
- skillSlug: item.skill.slug,
774
- skillName: item.skill.name,
775
- artifactName: item.publishedArtifact.frontmatterName,
776
- artifactDescription: item.publishedArtifact.frontmatterDescription,
777
- whenToUse: item.skill.whenToUse ?? null,
778
- version: item.skillVersion.version,
779
- assignmentSource: item.assignmentSource,
780
- assignmentType: item.assignmentType,
781
- scopePath: item.scopePath,
782
- includeChildren: item.includeChildren ?? null,
783
- checksum: item.publishedArtifact.checksum,
784
- publishedAt: item.publishedArtifact.publishedAt,
785
- fileCount: item.publishedArtifact.fileCount,
786
- identifiers: getSkillIdentifiers(item),
787
- tags: item.skill.tags.map(toPublishedSkillTagSummary),
788
- contextKind,
789
- installPolicy: item.skill.installPolicy,
790
- assignmentLabel: getPublishedSkillAssignmentLabel(item.assignmentSource),
791
- policyLabel: getSkillPolicyLabel(item.skill.installPolicy, contextKind, item.assignmentSource)
792
- };
793
- };
794
- export const toPublishedSkillDetail = item => ({
795
- ...toPublishedSkillSummary(item),
796
- skillId: item.skill.id,
797
- skillVersionId: item.skillVersion.id,
798
- artifactId: item.publishedArtifact.id,
799
- markdownDocument: buildSkillMarkdown(item),
800
- markdownBody: item.publishedArtifact.markdownBody,
801
- renderedContent: item.publishedArtifact.renderedContent,
802
- files: item.publishedArtifact.files,
803
- resources: item.publishedArtifact.files.filter(file => file.relativePath !== 'SKILL.md')
804
- });
805
- const toInstallableSkillSummary = item => ({
806
- skillSlug: item.skill.slug,
807
- skillName: item.skill.name,
808
- artifactName: item.publishedArtifact.frontmatterName,
809
- artifactDescription: item.publishedArtifact.frontmatterDescription,
810
- whenToUse: item.skill.whenToUse ?? null,
811
- version: item.skillVersion.version,
812
- assignmentSource: 'CATALOG',
813
- assignmentType: 'PATH',
814
- scopePath: '',
815
- includeChildren: true,
816
- checksum: item.publishedArtifact.checksum,
817
- publishedAt: item.publishedArtifact.publishedAt,
818
- fileCount: item.publishedArtifact.fileCount,
819
- identifiers: getSkillIdentifiers({
820
- ...item,
821
- assignmentSource: 'CATALOG',
822
- assignmentType: 'PATH',
823
- scopePath: '',
824
- includeChildren: true
825
- }),
826
- tags: item.skill.tags.map(toPublishedSkillTagSummary),
827
- contextKind: 'installable',
828
- installPolicy: item.skill.installPolicy,
829
- assignmentLabel: 'catalog skill',
830
- policyLabel: getSkillPolicyLabel(item.skill.installPolicy, 'installable', 'CATALOG')
831
- });
832
- export const toPublishedSkillCatalog = payload => ({
833
- pluginId: PLUGIN_ID,
834
- runtimeMode: 'tool_fetch_only',
835
- deliveryModel: 'backend_published_installed_effective_skills',
836
- workspace: payload.workspace,
837
- directoryPath: payload.directoryPath,
838
- rootSkillSeedPath: ROOT_SKILL_SEED_PATH,
839
- availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
840
- publishedSkillCount: payload.skills.length,
841
- assignmentCounts: getPublishedSkillAssignmentCounts(payload.skills),
842
- facets: getPublishedSkillFacets(payload.skills),
843
- skills: payload.skills.map(toPublishedSkillSummary)
844
- });
845
- const filterIgnoredPublishedSkills = async (config, result) => {
846
- const authState = await readGlobalAuthState(config.authStatePath);
847
- const userKey = toStoredUserKey(authState);
848
- if (!result.fetchResult.ok) {
849
- return {
850
- ...result,
851
- ignoreState: {
852
- scopeKey: getPublishedSkillIgnoreScopeKey(result.workspaceResolution),
853
- userKey,
854
- ignoredSkillSlugs: [],
855
- installedGlobalSkillSlugs: [],
856
- installedWorkspaceSkillSlugs: []
857
- },
858
- ignoredSkills: []
859
- };
860
- }
861
- const ignoredSkills = result.fetchResult.payload.userPreferences.ignoredSkills.map(toPublishedSkillSummary);
862
- const ignoredSkillSlugs = ignoredSkills.map(skill => skill.skillSlug);
863
- return {
864
- ...result,
865
- ignoreState: {
866
- scopeKey: result.fetchResult.payload.userPreferences.scopeKey,
867
- userKey: result.fetchResult.payload.userPreferences.userKey || userKey,
868
- ignoredSkillSlugs,
869
- installedGlobalSkillSlugs: [],
870
- installedWorkspaceSkillSlugs: []
871
- },
872
- ignoredSkills
873
- };
874
- };
875
- const getWorkspaceUnavailableMessage = payload => {
876
- if (payload.workspace) return null;
877
- return 'Workspace-specific skills are unavailable because the workspace was not found; global skills are still loaded.';
878
- };
879
- const normalizeSkillIdentifier = value => value.trim().toLowerCase();
880
- const parseSkillIdentifiers = value => {
881
- const seen = new Set();
882
- return value.split(/[\n,]/).map(item => item.trim()).filter(item => item.length > 0).filter(item => {
883
- const normalized = normalizeSkillIdentifier(item);
884
- if (seen.has(normalized)) return false;
885
- seen.add(normalized);
886
- return true;
887
- });
888
- };
889
- const mergeSkillIdentifiers = values => {
890
- const seen = new Set();
891
- return values.filter(value => {
892
- const normalized = normalizeSkillIdentifier(value);
893
- if (!normalized || seen.has(normalized)) return false;
894
- seen.add(normalized);
895
- return true;
896
- });
897
- };
898
- export const parseRequestedSkillArgs = args => {
899
- const requestedSkills = [];
900
- if (typeof args.skill === 'string') {
901
- requestedSkills.push(...parseSkillIdentifiers(args.skill));
902
- }
903
- if (typeof args.skills === 'string') {
904
- requestedSkills.push(...parseSkillIdentifiers(args.skills));
905
- }
906
- return mergeSkillIdentifiers(requestedSkills);
907
- };
908
- export const selectPublishedSkills = (payload, identifiers) => {
909
- const itemsByIdentifier = new Map();
910
- for (const item of payload.skills) {
911
- for (const identifier of getSkillIdentifiers(item)) {
912
- itemsByIdentifier.set(normalizeSkillIdentifier(identifier), item);
913
- }
914
- }
915
- const selectedItems = [];
916
- const selectedKeys = new Set();
917
- const missingIdentifiers = [];
918
- for (const identifier of identifiers) {
919
- const matched = itemsByIdentifier.get(normalizeSkillIdentifier(identifier));
920
- if (!matched) {
921
- missingIdentifiers.push(identifier);
922
- continue;
923
- }
924
- if (selectedKeys.has(matched.publishedArtifact.id)) {
925
- continue;
926
- }
927
- selectedKeys.add(matched.publishedArtifact.id);
928
- selectedItems.push(matched);
929
- }
930
- return {
931
- selectedItems,
932
- missingIdentifiers
933
- };
934
- };
935
- const truncateText = (value, maxLength) => {
936
- const normalized = value.replace(/\s+/gu, ' ').trim();
937
- if (normalized.length <= maxLength) return normalized;
938
- return `${normalized.slice(0, Math.max(maxLength - 1, 0)).trimEnd()}…`;
939
- };
940
- const buildSkillCatalogLine = skill => {
941
- const description = truncateText(skill.whenToUse || skill.artifactDescription || skill.skillName || skill.skillSlug, SYSTEM_NOTE_SKILL_DESCRIPTION_LIMIT);
942
- const scopeLabel = isUserPublishedSkillAssignment(skill.assignmentSource) ? 'USER SCOPE' : skill.contextKind === 'global' ? 'GLOBAL SCOPE' : 'PROJECT SCOPE';
943
- const assignmentLabel = skill.assignmentSource.toLowerCase().replace(/_/gu, ' ');
944
- return `- ${skill.artifactName || skill.skillName} (${skill.skillSlug}, ${assignmentLabel} assignment) [${scopeLabel}]: ${description}`;
945
- };
946
- const buildSkillDetailSnippetLine = detail => {
947
- const body = detail.markdownBody || detail.renderedContent || detail.markdownDocument;
948
- return `- ${detail.artifactName || detail.skillName}: ${truncateText(body, 700)}`;
949
- };
950
- export const buildSystemNote = (result, config, details) => {
951
- if (!result.fetchResult.ok) return null;
952
- const catalog = toPublishedSkillCatalog(result.fetchResult.payload);
953
- const skillNames = catalog.skills.map(skill => skill.artifactName || skill.skillName || skill.skillSlug);
954
- const renderedSkillNames = skillNames.length > 0 ? skillNames.slice(0, SYSTEM_NOTE_SKILL_NAME_LIMIT).join(', ') : 'none';
955
- const remainingCount = Math.max(skillNames.length - SYSTEM_NOTE_SKILL_NAME_LIMIT, 0);
956
- const renderedCountSuffix = remainingCount > 0 ? ` (+${remainingCount} more)` : '';
957
- const globalSkills = catalog.skills.filter(skill => skill.contextKind === 'global' && !isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 8).map(buildSkillCatalogLine);
958
- const projectSkills = catalog.skills.filter(skill => skill.contextKind === 'project' && !isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 5).map(buildSkillCatalogLine);
959
- const userSkills = catalog.skills.filter(skill => isUserPublishedSkillAssignment(skill.assignmentSource)).slice(0, 5).map(buildSkillCatalogLine);
960
- const detailLines = details.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(buildSkillDetailSnippetLine);
961
- const detailBlock = detailLines.length > 0 ? `Loaded body snippets (capped):\n${truncateText(detailLines.join('\n'), SYSTEM_NOTE_DETAIL_CHAR_LIMIT)}` : '';
962
- return [result.fetchResult.payload.workspace ? `Workspace: ${result.fetchResult.payload.workspace.slug}.` : 'Workspace not found; workspace-scoped wizard skills are unavailable.', `Current directory: ${result.directoryPath}.`, `Active wizard skills: ${renderedSkillNames}${renderedCountSuffix}.`, `Counts: ${catalog.assignmentCounts.global} global, ${catalog.assignmentCounts.project} project, ${catalog.assignmentCounts.user} user, ${catalog.assignmentCounts.other} other.`, 'Wizard-listed skills are backend-published, not native OpenCode skills.', 'Use native skill tooling only for names in native available_skills.', 'When a wizard skill matches by whenToUse, fetch its current body with opencode_wizard_published_skills_fetch before using it.', 'Use `skills` for multiple wizard skill identifiers; use `skill` for one.', 'If native skill loading cannot find a wizard-listed skill, fetch it as a wizard skill instead.', 'Fetched wizard bodies are authoritative over local seed/native sources.', globalSkills.length > 0 ? `Global skills:\n${globalSkills.join('\n')}` : 'Global skills: none.', projectSkills.length > 0 ? `Project skills:\n${projectSkills.join('\n')}` : 'Project skills: none.', userSkills.length > 0 ? `User skills:\n${userSkills.join('\n')}` : 'User skills: none.', detailBlock].filter(Boolean).join(' ');
963
- };
964
- const toWorkspaceResolutionOutput = resolution => ({
965
- requestedDirectory: resolution.requestedDirectory,
966
- repositoryRoot: resolution.repositoryRoot,
967
- repositoryUrl: resolution.repositoryUrl,
968
- workspaceSlug: resolution.workspaceSlug,
969
- workspaceSlugSource: resolution.workspaceSlugSource,
970
- fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug,
971
- directoryPath: resolution.directoryPath
972
- });
973
- const toWorkspaceResolutionMetadata = resolution => ({
974
- directoryPath: resolution.directoryPath,
975
- repositoryRoot: resolution.repositoryRoot,
976
- repositoryUrl: resolution.repositoryUrl ?? '',
977
- workspaceSlug: resolution.workspaceSlug ?? '',
978
- workspaceSlugSource: resolution.workspaceSlugSource ?? 'placeholder',
979
- fallbackWorkspaceSlug: resolution.fallbackWorkspaceSlug ?? ''
980
- });
981
- const formatStatusOutput = async (worktree, config, publishedSkillsResult, loginBootstrapSnapshot) => {
982
- const authState = await resolveStoredAuthState(worktree, config);
983
- const filteredResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
984
- const base = {
985
- pluginId: PLUGIN_ID,
986
- runtimeMode: 'tool_fetch_only',
987
- nativeSkillsUrlCompatibility: NATIVE_SKILLS_URL_COMPATIBILITY,
988
- backendOrigin: config.backendOrigin,
989
- graphqlUrl: config.graphqlUrl,
990
- fallbackWorkspaceSlug: config.fallbackWorkspaceSlug,
991
- workspaceResolution: toWorkspaceResolutionOutput(publishedSkillsResult.workspaceResolution),
992
- rootSkillSeedPath: config.rootSkillSeedPath,
993
- authStatePath: config.authStatePath,
994
- requestedDirectoryPath: publishedSkillsResult.directoryPath,
995
- authMode: publishedSkillsResult.fetchResult.authMode,
996
- authState: authState === null ? null : {
997
- email: authState.email,
998
- userId: authState.userId,
999
- authenticatedAt: authState.authenticatedAt,
1000
- expiresAt: authState.expiresAt
1001
- },
1002
- loginBootstrap: loginBootstrapSnapshot.status === 'idle' ? null : {
1003
- status: loginBootstrapSnapshot.status,
1004
- trigger: loginBootstrapSnapshot.trigger,
1005
- startedAt: loginBootstrapSnapshot.startedAt,
1006
- expiresAt: loginBootstrapSnapshot.expiresAt,
1007
- browserUrl: loginBootstrapSnapshot.browserUrl,
1008
- browserOpenError: loginBootstrapSnapshot.browserOpenError,
1009
- email: loginBootstrapSnapshot.email,
1010
- message: loginBootstrapSnapshot.message
1011
- },
1012
- status: filteredResult.fetchResult.status,
1013
- fetchedAt: filteredResult.fetchResult.fetchedAt,
1014
- source: filteredResult.fetchResult.source,
1015
- availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
1016
- ignoredPublishedSkills: {
1017
- scopeKey: filteredResult.ignoreState.scopeKey,
1018
- userKey: filteredResult.ignoreState.userKey,
1019
- count: filteredResult.ignoreState.ignoredSkillSlugs.length
1020
- }
1021
- };
1022
- if (!filteredResult.fetchResult.ok) {
1023
- return JSON.stringify({
1024
- ...base,
1025
- message: filteredResult.fetchResult.message
1026
- }, null, 2);
1027
- }
1028
- return JSON.stringify({
1029
- ...base,
1030
- ...toPublishedSkillCatalog(filteredResult.fetchResult.payload),
1031
- message: getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload)
1032
- }, null, 2);
1033
- };
1034
- export const toPluginAuthStateSummary = authState => {
1035
- if (!authState) {
1036
- return {
1037
- status: 'missing',
1038
- email: null,
1039
- userId: null,
1040
- authenticatedAt: null,
1041
- expiresAt: null
1042
- };
1043
- }
1044
- return {
1045
- status: 'authenticated',
1046
- email: authState.email,
1047
- userId: authState.userId,
1048
- authenticatedAt: authState.authenticatedAt,
1049
- expiresAt: authState.expiresAt
1050
- };
1051
- };
1052
- export const resolvePluginStatusSnapshot = async ({
1053
- worktree,
1054
- directory,
1055
- signal
1056
- }) => {
1057
- const config = await resolveConfig(worktree);
1058
- const workspaceResolution = await resolveWorkspace({
1059
- config,
1060
- directory
1061
- });
1062
- const fetchResult = await fetchPublishedSkillsCatalog(worktree, config, workspaceResolution, signal);
1063
- const filteredResult = await filterIgnoredPublishedSkills(config, {
1064
- directoryPath: workspaceResolution.directoryPath,
1065
- workspaceResolution,
1066
- fetchResult
1067
- });
1068
- const authState = await resolveStoredAuthState(worktree, config);
1069
- return {
1070
- pluginId: PLUGIN_ID,
1071
- runtimeMode: 'tool_fetch_only',
1072
- nativeSkillsUrlCompatibility: NATIVE_SKILLS_URL_COMPATIBILITY,
1073
- backendOrigin: config.backendOrigin,
1074
- graphqlUrl: config.graphqlUrl,
1075
- fallbackWorkspaceSlug: config.fallbackWorkspaceSlug,
1076
- workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
1077
- rootSkillSeedPath: config.rootSkillSeedPath,
1078
- authStatePath: config.authStatePath,
1079
- authState: toPluginAuthStateSummary(authState),
1080
- status: filteredResult.fetchResult.status,
1081
- authMode: filteredResult.fetchResult.authMode,
1082
- fetchedAt: filteredResult.fetchResult.fetchedAt,
1083
- source: filteredResult.fetchResult.source,
1084
- availableTools: AVAILABLE_PUBLISHED_SKILL_TOOLS,
1085
- message: filteredResult.fetchResult.ok ? getWorkspaceUnavailableMessage(filteredResult.fetchResult.payload) : filteredResult.fetchResult.message,
1086
- catalog: filteredResult.fetchResult.ok ? toPublishedSkillCatalog(filteredResult.fetchResult.payload) : null,
1087
- installableCatalog: filteredResult.fetchResult.ok ? {
1088
- count: filteredResult.fetchResult.payload.catalogSkills.length,
1089
- skills: filteredResult.fetchResult.payload.catalogSkills.map(toInstallableSkillSummary)
1090
- } : null,
1091
- ignoredPublishedSkills: {
1092
- scopeKey: filteredResult.ignoreState.scopeKey,
1093
- userKey: filteredResult.ignoreState.userKey,
1094
- count: filteredResult.ignoreState.ignoredSkillSlugs.length,
1095
- skills: filteredResult.ignoredSkills
1096
- }
1097
- };
1098
- };
1099
- const withStatusMessage = (snapshot, message) => ({
1100
- ...snapshot,
1101
- message
1102
- });
1103
- const toAiFacingPluginStatusSnapshot = snapshot => {
1104
- const {
1105
- ignoredPublishedSkills,
1106
- installableCatalog: _installableCatalog,
1107
- ...safeSnapshot
1108
- } = snapshot;
1109
- return {
1110
- ...safeSnapshot,
1111
- ignoredPublishedSkills: {
1112
- scopeKey: ignoredPublishedSkills.scopeKey,
1113
- count: ignoredPublishedSkills.count
1114
- }
1115
- };
1116
- };
1117
- const startStatusPathLoginBootstrap = (worktree, config) => {
1118
- if (statusPathLoginBootstrap.promise) return;
1119
- if (statusPathLoginBootstrap.status === 'failed' && statusPathLoginBootstrap.failedAt && Date.now() - statusPathLoginBootstrap.failedAt < STATUS_PATH_LOGIN_RETRY_COOLDOWN_MS) {
1120
- return;
1121
- }
1122
- statusPathLoginBootstrap.status = 'pending';
1123
- statusPathLoginBootstrap.message = 'Browser login started automatically from the TUI/status path.';
1124
- statusPathLoginBootstrap.failedAt = null;
1125
- statusPathLoginBootstrap.promise = (async () => {
1126
- const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
1127
- const loginStart = await startLoginFlow(loginSignal);
1128
- const browserOpenError = await openBrowser(loginStart.browserUrl);
1129
- if (browserOpenError) {
1130
- statusPathLoginBootstrap.message = `Automatic browser open failed. Open ${loginStart.browserUrl} manually.`;
1131
- }
1132
- try {
1133
- const callbackPayload = await loginStart.callbackPromise;
1134
- if (callbackPayload.status === 'error') {
1135
- throw new Error(callbackPayload.message);
1136
- }
1137
- if (callbackPayload.state !== loginStart.expectedState) {
1138
- throw new Error('OAuth callback state did not match the original login request.');
1139
- }
1140
- const pluginSession = await createPluginSession({
1141
- code: callbackPayload.code,
1142
- codeVerifier: loginStart.codeVerifier,
1143
- redirectUri: OIDC_CALLBACK_URL,
1144
- config,
1145
- signal: loginSignal
1146
- });
1147
- const authState = toAuthState(pluginSession);
1148
- await writeAuthState(config.authStatePath, authState);
1149
- statusPathLoginBootstrap.status = 'authenticated';
1150
- statusPathLoginBootstrap.message = `Browser login completed successfully for ${authState.email}.`;
1151
- return authState;
1152
- } finally {
1153
- await loginStart.closeCallbackServer().catch(() => undefined);
1154
- }
1155
- })().catch(error => {
1156
- statusPathLoginBootstrap.status = 'failed';
1157
- statusPathLoginBootstrap.failedAt = Date.now();
1158
- statusPathLoginBootstrap.message = error instanceof Error ? error.message : 'Browser login failed.';
1159
- throw error;
1160
- }).finally(() => {
1161
- statusPathLoginBootstrap.promise = null;
1162
- });
1163
- statusPathLoginBootstrap.promise.catch(() => undefined);
1164
- };
1165
- export const resolvePluginStatusSnapshotWithAuthBootstrap = async ({
1166
- worktree,
1167
- directory,
1168
- signal
1169
- }) => {
1170
- const snapshot = await resolvePluginStatusSnapshot({
1171
- worktree,
1172
- directory,
1173
- signal
1174
- });
1175
- if (snapshot.status !== 'missing_auth') return snapshot;
1176
- const config = await resolveConfig(worktree);
1177
- startStatusPathLoginBootstrap(worktree, config);
1178
- if (statusPathLoginBootstrap.message) {
1179
- return withStatusMessage(snapshot, statusPathLoginBootstrap.message);
1180
- }
1181
- return withStatusMessage(snapshot, 'Browser login is pending from the TUI/status path.');
1182
- };
1183
- const toBackendPreferenceScope = preferenceScope => {
1184
- if (preferenceScope === 'global') return 'GLOBAL';
1185
- return 'WORKSPACE';
1186
- };
1187
- const setPublishedSkillPreference = async ({
1188
- worktree,
1189
- directory,
1190
- config,
1191
- skillSlug,
1192
- preferenceScope,
1193
- installed,
1194
- ignored
1195
- }) => {
1196
- const workspaceResolution = await resolveWorkspace({
1197
- config,
1198
- directory
1199
- });
1200
- const response = await fetchPublishedSkillsGraphQl({
1201
- worktree,
1202
- config,
1203
- query: SET_PUBLISHED_SKILL_PREFERENCE_MUTATION,
1204
- variables: {
1205
- input: {
1206
- ...toDeliveryInput(workspaceResolution),
1207
- skillSlug,
1208
- preferenceScope: toBackendPreferenceScope(preferenceScope),
1209
- installed,
1210
- ignored
1211
- }
1212
- },
1213
- signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
1214
- });
1215
- if (!response.ok) {
1216
- throw new Error(response.result.message);
1217
- }
1218
- const preferences = response.data.setPublishedSkillPreference;
1219
- publishedSkillPreferenceCacheVersion += 1;
1220
- return {
1221
- scopeKey: preferences.scopeKey,
1222
- userKey: preferences.userKey,
1223
- ignoredSkillSlugs: preferences.ignoredSkills.map(item => item.skill.slug),
1224
- installedGlobalSkillSlugs: [],
1225
- installedWorkspaceSkillSlugs: []
1226
- };
1227
- };
1228
- export const setPublishedSkillIgnored = async ({
1229
- worktree,
1230
- directory,
1231
- skillSlug,
1232
- ignored,
1233
- preferenceScope
1234
- }) => {
1235
- const config = await resolveConfig(worktree);
1236
- const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
1237
- if (!normalizedSkillSlug) {
1238
- throw new Error('Cannot toggle an empty published skill slug.');
1239
- }
1240
- return setPublishedSkillPreference({
1241
- worktree,
1242
- directory,
1243
- config,
1244
- skillSlug: normalizedSkillSlug,
1245
- preferenceScope: preferenceScope ?? 'project',
1246
- ignored
1247
- });
1248
- };
1249
- export const setPublishedSkillInstalled = async ({
1250
- worktree,
1251
- directory,
1252
- skillSlug,
1253
- installed,
1254
- preferenceScope
1255
- }) => {
1256
- const config = await resolveConfig(worktree);
1257
- const normalizedSkillSlug = toIgnoredSkillSlug(skillSlug);
1258
- if (!normalizedSkillSlug) {
1259
- throw new Error('Cannot toggle an empty published skill slug.');
1260
- }
1261
- return setPublishedSkillPreference({
1262
- worktree,
1263
- directory,
1264
- config,
1265
- skillSlug: normalizedSkillSlug,
1266
- preferenceScope,
1267
- installed
1268
- });
1269
- };
1270
- const toPluginStatusMetadata = snapshot => ({
1271
- backendOrigin: snapshot.backendOrigin,
1272
- graphqlUrl: snapshot.graphqlUrl,
1273
- pluginStatus: snapshot.status,
1274
- authStatus: snapshot.authState.status,
1275
- authEmail: snapshot.authState.email ?? '',
1276
- authUserId: snapshot.authState.userId ?? '',
1277
- directoryPath: snapshot.workspaceResolution.directoryPath,
1278
- repositoryUrl: snapshot.workspaceResolution.repositoryUrl ?? '',
1279
- source: snapshot.source,
1280
- workspaceSlug: snapshot.workspaceResolution.workspaceSlug ?? '',
1281
- workspaceSlugSource: snapshot.workspaceResolution.workspaceSlugSource ?? 'placeholder'
1282
- });
1283
- const isUnauthorizedGraphQlMessage = message => {
1284
- const normalizedMessage = message.toLowerCase();
1285
- return normalizedMessage.includes('not authorized') || normalizedMessage.includes('unauthorized');
1286
- };
1287
- const createRandomBase64Url = bytes => {
1288
- return crypto.randomBytes(bytes).toString('base64url');
1289
- };
1290
- const createCodeChallenge = codeVerifier => {
1291
- return crypto.createHash('sha256').update(codeVerifier).digest('base64url');
1292
- };
1293
- const getMessageFromUnknownPayload = value => {
1294
- if (!value || typeof value !== 'object') return null;
1295
- const candidate = 'message' in value ? value.message : null;
1296
- return typeof candidate === 'string' ? candidate : null;
1297
- };
1298
- const wait = async milliseconds => {
1299
- await new Promise(resolve => {
1300
- setTimeout(resolve, milliseconds);
1301
- });
1302
- };
1303
- const shouldRetryPresenceEvent = status => {
1304
- return status === 408 || status === 429 || status >= 500;
1305
- };
1306
- const fetchOidcDiscoveryDocument = async signal => {
1307
- const discoveryUrl = `${OIDC_ISSUER.replace(/\/+$/, '')}/.well-known/openid-configuration`;
1308
- const response = await fetch(discoveryUrl, {
1309
- method: 'GET',
1310
- signal
1311
- });
1312
- if (!response.ok) {
1313
- throw new Error(`OIDC discovery failed with HTTP ${response.status}.`);
1314
- }
1315
- return await response.json();
1316
- };
1317
- const isCallbackPortInUseError = error => {
1318
- if (!error || typeof error !== 'object') return false;
1319
- if (!('code' in error)) return false;
1320
- return error.code === 'EADDRINUSE';
1321
- };
1322
- const toCallbackServerStartError = error => {
1323
- if (!isCallbackPortInUseError(error)) {
1324
- return error instanceof Error ? error : new Error('Failed to start local OAuth callback server.');
1325
- }
1326
- return new Error('OAuth login cannot start because localhost:24953 is already in use. Another OpenCode login is likely in progress; finish it or close the other instance, then retry.');
1327
- };
1328
- const startLocalCallbackServer = async ({
1329
- expectedState,
1330
- signal
1331
- }) => {
1332
- let settled = false;
1333
- let resolvePayload = () => undefined;
1334
- let rejectPayload = () => undefined;
1335
- const callbackPromise = new Promise((resolve, reject) => {
1336
- resolvePayload = resolve;
1337
- rejectPayload = reject;
1338
- });
1339
- const finalize = payload => {
1340
- if (settled) return;
1341
- settled = true;
1342
- resolvePayload(payload);
1343
- };
1344
- const fail = reason => {
1345
- if (settled) return;
1346
- settled = true;
1347
- rejectPayload(reason);
1348
- };
1349
- const server = http.createServer((request, response) => {
1350
- const requestUrl = new URL(request.url ?? '/', OIDC_CALLBACK_ORIGIN);
1351
- if (requestUrl.pathname !== OIDC_CALLBACK_PATH) {
1352
- sendOAuthCallbackHtmlResponse(response, 404, 'opencode-wizard plugin login', 'Unknown callback path.');
1353
- return;
1354
- }
1355
- const error = requestUrl.searchParams.get('error');
1356
- const errorDescription = requestUrl.searchParams.get('error_description');
1357
- if (error) {
1358
- const message = errorDescription ?? error;
1359
- sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', message);
1360
- finalize({
1361
- status: 'error',
1362
- message
1363
- });
1364
- return;
1365
- }
1366
- const state = requestUrl.searchParams.get('state');
1367
- const code = requestUrl.searchParams.get('code');
1368
- if (!state || state !== expectedState) {
1369
- sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth state did not match the login request.');
1370
- finalize({
1371
- status: 'error',
1372
- message: 'OAuth state did not match the login request.'
1373
- });
1374
- return;
1375
- }
1376
- if (!code) {
1377
- sendOAuthCallbackHtmlResponse(response, 400, 'opencode-wizard plugin login failed', 'OAuth callback did not include an authorization code.');
1378
- finalize({
1379
- status: 'error',
1380
- message: 'OAuth callback did not include an authorization code.'
1381
- });
1382
- return;
1383
- }
1384
- sendOAuthCallbackHtmlResponse(response, 200, 'opencode-wizard plugin callback received', 'Callback received. OpenCode is finalizing the backend session now.');
1385
- finalize({
1386
- status: 'success',
1387
- code,
1388
- state
1389
- });
1390
- });
1391
- const close = async () => {
1392
- await new Promise((resolve, reject) => {
1393
- server.close(error => {
1394
- if (error) {
1395
- reject(error);
1396
- return;
1397
- }
1398
- resolve();
1399
- });
1400
- });
1401
- };
1402
- await new Promise((resolve, reject) => {
1403
- const rejectStart = error => {
1404
- reject(toCallbackServerStartError(error));
1405
- };
1406
- server.once('error', rejectStart);
1407
- server.listen(24953, 'localhost', () => {
1408
- server.off('error', rejectStart);
1409
- server.on('error', error => {
1410
- fail(error instanceof Error ? error : new Error('Local OAuth callback server failed.'));
1411
- });
1412
- resolve();
1413
- });
1414
- });
1415
- signal.addEventListener('abort', () => {
1416
- fail(signal.reason instanceof Error ? signal.reason : new Error('OAuth login aborted.'));
1417
- void close().catch(() => undefined);
1418
- }, {
1419
- once: true
1420
- });
1421
- return {
1422
- callbackPromise,
1423
- close
1424
- };
1425
- };
1426
- const fetchPublishedSkillsGraphQl = async ({
1427
- worktree,
1428
- config,
1429
- query,
1430
- variables,
1431
- signal,
1432
- onAuthStateChanged
1433
- }) => {
1434
- const authState = await resolveStoredAuthState(worktree, config);
1435
- const fetchedAt = new Date().toISOString();
1436
- if (!authState) {
1437
- return {
1438
- ok: false,
1439
- result: {
1440
- ok: false,
1441
- status: 'missing_auth',
1442
- authMode: 'missing',
1443
- message: 'No plugin session is stored. Interactive opencode_wizard_published_skills_fetch can bootstrap browser login automatically when needed. Configured backend and GraphQL URLs are shown for visibility, but no GraphQL request was made because auth is missing.',
1444
- fetchedAt,
1445
- source: 'network'
1446
- }
1447
- };
1448
- }
1449
- let response;
1450
- try {
1451
- response = await fetch(config.graphqlUrl, {
1452
- method: 'POST',
1453
- headers: {
1454
- 'content-type': 'application/json',
1455
- authorization: `Bearer ${authState.sessionToken}`
1456
- },
1457
- body: JSON.stringify({
1458
- query,
1459
- variables
1460
- }),
1461
- signal
1462
- });
1463
- } catch (error) {
1464
- return {
1465
- ok: false,
1466
- result: {
1467
- ok: false,
1468
- status: 'request_failed',
1469
- authMode: 'session',
1470
- message: error instanceof Error ? error.message : 'Unknown fetch error',
1471
- fetchedAt,
1472
- source: 'network'
1473
- }
1474
- };
1475
- }
1476
- if (response.status === 401 || response.status === 403) {
1477
- await clearAuthState(config.authStatePath);
1478
- onAuthStateChanged?.();
1479
- return {
1480
- ok: false,
1481
- result: {
1482
- ok: false,
1483
- status: 'missing_auth',
1484
- authMode: 'session',
1485
- message: 'Stored plugin session was rejected by the backend. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
1486
- fetchedAt,
1487
- source: 'network'
1488
- }
1489
- };
1490
- }
1491
- if (!response.ok) {
1492
- return {
1493
- ok: false,
1494
- result: {
1495
- ok: false,
1496
- status: 'request_failed',
1497
- authMode: 'session',
1498
- message: `GraphQL request failed with HTTP ${response.status}.`,
1499
- fetchedAt,
1500
- source: 'network'
1501
- }
1502
- };
1503
- }
1504
- let body;
1505
- try {
1506
- body = await response.json();
1507
- } catch (error) {
1508
- return {
1509
- ok: false,
1510
- result: {
1511
- ok: false,
1512
- status: 'request_failed',
1513
- authMode: 'session',
1514
- message: `GraphQL response was not valid JSON: ${error instanceof Error ? error.message : 'Unknown parse error'}`,
1515
- fetchedAt,
1516
- source: 'network'
1517
- }
1518
- };
1519
- }
1520
- if (body.errors?.length) {
1521
- const message = body.errors.map(error => error.message).join('; ');
1522
- if (body.errors.some(error => isUnauthorizedGraphQlMessage(error.message))) {
1523
- await clearAuthState(config.authStatePath);
1524
- onAuthStateChanged?.();
1525
- return {
1526
- ok: false,
1527
- result: {
1528
- ok: false,
1529
- status: 'missing_auth',
1530
- authMode: 'session',
1531
- message: 'Stored plugin session is no longer valid. Retry opencode_wizard_published_skills_fetch to bootstrap a fresh browser login automatically.',
1532
- fetchedAt,
1533
- source: 'network'
1534
- }
1535
- };
1536
- }
1537
- return {
1538
- ok: false,
1539
- result: {
1540
- ok: false,
1541
- status: 'request_failed',
1542
- authMode: 'session',
1543
- message,
1544
- fetchedAt,
1545
- source: 'network'
1546
- }
1547
- };
1548
- }
1549
- if (!body.data) {
1550
- return {
1551
- ok: false,
1552
- result: {
1553
- ok: false,
1554
- status: 'request_failed',
1555
- authMode: 'session',
1556
- message: 'GraphQL response did not include data.',
1557
- fetchedAt,
1558
- source: 'network'
1559
- }
1560
- };
1561
- }
1562
- return {
1563
- ok: true,
1564
- data: body.data,
1565
- fetchedAt
1566
- };
1567
- };
1568
- const fetchPublishedSkillsCatalog = async (worktree, config, resolution, signal, onAuthStateChanged) => {
1569
- const response = await fetchPublishedSkillsGraphQl({
1570
- worktree,
1571
- config,
1572
- query: PUBLISHED_SKILLS_CATALOG_QUERY,
1573
- variables: {
1574
- input: toDeliveryInput(resolution)
1575
- },
1576
- signal,
1577
- onAuthStateChanged
1578
- });
1579
- if (!response.ok) {
1580
- return response.result;
1581
- }
1582
- const payload = response.data.pluginPublishedSkills;
1583
- if (!payload) {
1584
- return {
1585
- ok: false,
1586
- status: 'request_failed',
1587
- authMode: 'session',
1588
- message: 'GraphQL response did not include pluginPublishedSkills.',
1589
- fetchedAt: response.fetchedAt,
1590
- source: 'network'
1591
- };
1592
- }
1593
- return {
1594
- ok: true,
1595
- status: 'ready',
1596
- authMode: 'session',
1597
- payload,
1598
- fetchedAt: response.fetchedAt,
1599
- source: 'network'
1600
- };
1601
- };
1602
- const maybePersistWorkspaceSlugFromCatalog = async ({
1603
- config,
1604
- resolution,
1605
- fetchResult
1606
- }) => {
1607
- if (!fetchResult.ok) return;
1608
- const backendWorkspaceSlug = fetchResult.payload.workspace?.slug?.trim();
1609
- if (!backendWorkspaceSlug) return;
1610
- const normalizedWorkspaceSlug = toWorkspaceSlug(backendWorkspaceSlug);
1611
- if (!normalizedWorkspaceSlug) return;
1612
- if (resolution.workspaceSlug === normalizedWorkspaceSlug && resolution.workspaceSlugSource !== 'fallback') return;
1613
- await upsertWorkspaceSlugMapping({
1614
- configFile: config.authStatePath,
1615
- repositoryUrl: resolution.repositoryUrl,
1616
- repositoryRoot: resolution.repositoryRoot,
1617
- workspaceSlug: normalizedWorkspaceSlug
1618
- });
1619
- };
1620
- const fetchPublishedSkillDetail = async ({
1621
- worktree,
1622
- config,
1623
- resolution,
1624
- skillVersionId,
1625
- signal,
1626
- onAuthStateChanged,
1627
- purpose
1628
- }) => {
1629
- const response = await fetchPublishedSkillsGraphQl({
1630
- worktree,
1631
- config,
1632
- query: PUBLISHED_SKILL_DETAIL_QUERY,
1633
- variables: {
1634
- input: {
1635
- ...toDeliveryInput(resolution),
1636
- skillVersionId,
1637
- purpose
1638
- }
1639
- },
1640
- signal,
1641
- onAuthStateChanged
1642
- });
1643
- if (!response.ok) {
1644
- return response;
1645
- }
1646
- const artifact = response.data.pluginPublishedSkillVersionArtifact;
1647
- if (!artifact) {
1648
- return {
1649
- ok: false,
1650
- result: {
1651
- ok: false,
1652
- status: 'not_found',
1653
- authMode: 'session',
1654
- message: 'Published skill detail is not effective for the current scope.',
1655
- fetchedAt: response.fetchedAt,
1656
- source: 'network'
1657
- }
1658
- };
1659
- }
1660
- return {
1661
- ok: true,
1662
- artifact
1663
- };
1664
- };
1665
- const toFetchFailureOutput = async ({
1666
- worktree,
1667
- config,
1668
- publishedSkillsResult,
1669
- loginBootstrapSnapshot
1670
- }) => ({
1671
- output: await formatStatusOutput(worktree, config, publishedSkillsResult, loginBootstrapSnapshot),
1672
- metadata: {
1673
- status: publishedSkillsResult.fetchResult.status,
1674
- ...toWorkspaceResolutionMetadata(publishedSkillsResult.workspaceResolution),
1675
- source: publishedSkillsResult.fetchResult.source
1676
- }
1677
- });
1678
- const startLoginFlow = async signal => {
1679
- const discovery = await fetchOidcDiscoveryDocument(signal);
1680
- const codeVerifier = createRandomBase64Url(64);
1681
- const expectedState = createRandomBase64Url(32);
1682
- const codeChallenge = createCodeChallenge(codeVerifier);
1683
- const expiresAt = new Date(Date.now() + LOGIN_TIMEOUT_MS).toISOString();
1684
- const {
1685
- callbackPromise,
1686
- close
1687
- } = await startLocalCallbackServer({
1688
- expectedState,
1689
- signal
1690
- });
1691
- const browserUrl = new URL(discovery.authorization_endpoint);
1692
- browserUrl.searchParams.set('client_id', OIDC_CLIENT_ID);
1693
- browserUrl.searchParams.set('response_type', 'code');
1694
- browserUrl.searchParams.set('redirect_uri', OIDC_CALLBACK_URL);
1695
- browserUrl.searchParams.set('response_mode', 'query');
1696
- browserUrl.searchParams.set('scope', OIDC_SCOPES.join(' '));
1697
- browserUrl.searchParams.set('code_challenge', codeChallenge);
1698
- browserUrl.searchParams.set('code_challenge_method', 'S256');
1699
- browserUrl.searchParams.set('state', expectedState);
1700
- return {
1701
- browserUrl: browserUrl.toString(),
1702
- expiresAt,
1703
- codeVerifier,
1704
- expectedState,
1705
- callbackPromise,
1706
- closeCallbackServer: close
1707
- };
1708
- };
1709
- const createPluginSession = async ({
1710
- code,
1711
- codeVerifier,
1712
- redirectUri,
1713
- config,
1714
- signal
1715
- }) => {
1716
- const response = await fetch(config.authSessionUrl, {
1717
- method: 'POST',
1718
- headers: {
1719
- 'content-type': 'application/json'
1720
- },
1721
- body: JSON.stringify({
1722
- code,
1723
- codeVerifier,
1724
- redirectUri
1725
- }),
1726
- signal
1727
- });
1728
- const payload = await response.json().catch(() => null);
1729
- if (!response.ok) {
1730
- throw new Error(getMessageFromUnknownPayload(payload) ?? `Plugin session exchange failed with HTTP ${response.status}.`);
1731
- }
1732
- if (!payload || !('success' in payload) || payload.success !== true) {
1733
- throw new Error('Plugin session exchange returned an unexpected payload.');
1734
- }
1735
- return payload.session;
1736
- };
1737
- const emitPresenceEvent = async ({
1738
- config,
1739
- authState,
1740
- event,
1741
- workspacePath
1742
- }) => {
1743
- for (let attempt = 1; attempt <= PRESENCE_EVENT_MAX_ATTEMPTS; attempt += 1) {
1744
- try {
1745
- const response = await fetch(config.presenceUrl, {
1746
- method: 'POST',
1747
- headers: {
1748
- 'content-type': 'application/json',
1749
- authorization: `Bearer ${authState.sessionToken}`
1750
- },
1751
- body: JSON.stringify({
1752
- event,
1753
- occurredAt: new Date().toISOString(),
1754
- workspacePath
1755
- }),
1756
- keepalive: event === 'STOP',
1757
- signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
1758
- });
1759
- if (response.ok) return;
1760
- if (!shouldRetryPresenceEvent(response.status) || attempt === PRESENCE_EVENT_MAX_ATTEMPTS) return;
1761
- } catch {
1762
- if (attempt === PRESENCE_EVENT_MAX_ATTEMPTS) return;
1763
- }
1764
- await wait(PRESENCE_EVENT_RETRY_DELAY_MS * attempt);
1765
- }
1766
- };
1767
- const emitPluginActionEvent = async ({
1768
- config,
1769
- authState,
1770
- event,
1771
- workspacePath,
1772
- directoryPath
1773
- }) => {
1774
- if (!authState) return;
1775
- try {
1776
- await fetch(config.actionsUrl, {
1777
- method: 'POST',
1778
- headers: {
1779
- 'content-type': 'application/json',
1780
- authorization: `Bearer ${authState.sessionToken}`
1781
- },
1782
- body: JSON.stringify({
1783
- event,
1784
- occurredAt: new Date().toISOString(),
1785
- workspacePath,
1786
- directoryPath
1787
- }),
1788
- keepalive: event === 'STOP',
1789
- signal: AbortSignal.timeout(PRESENCE_EVENT_TIMEOUT_MS)
1790
- });
1791
- } catch {
1792
- return;
1793
- }
1794
- };
1795
- const openBrowser = async url => {
1796
- try {
1797
- if (process.platform === 'darwin') {
1798
- await execFileAsync('open', [url]);
1799
- return null;
1800
- }
1801
- if (process.platform === 'win32') {
1802
- await execFileAsync('cmd', ['/c', 'start', '', url]);
1803
- return null;
1804
- }
1805
- await execFileAsync('xdg-open', [url]);
1806
- return null;
1807
- } catch (error) {
1808
- return error instanceof Error ? error.message : 'Failed to open browser automatically';
1809
- }
1810
- };
1811
- const normalizeDirectoryArg = (contextDirectory, directory) => {
1812
- return normalizeAbsolutePath(directory ? path.resolve(contextDirectory, directory) : contextDirectory);
1813
- };
1814
- const getDetailCacheKey = (catalogCacheKey, skillVersionId) => {
1815
- return JSON.stringify([catalogCacheKey, skillVersionId]);
1816
- };
1817
- const getDetailInflightKey = (catalogCacheKey, skillVersionId, purpose) => {
1818
- return JSON.stringify([catalogCacheKey, skillVersionId, purpose]);
1819
- };
1820
- const OpencodeWizardSkillsPlugin = async input => {
1821
- const {
1822
- tool
1823
- } = await importOpencodePluginModule('@opencode-ai/plugin');
1824
- const config = await resolveConfig(input.worktree);
1825
- const workspacePath = normalizeAbsolutePath(input.worktree);
1826
- const cache = new Map();
1827
- const catalogInflight = new Map();
1828
- const detailCache = new Map();
1829
- const detailInflight = new Map();
1830
- const initialAuthState = await resolveStoredAuthState(input.worktree, config);
1831
- const loginBootstrap = {
1832
- promise: null,
1833
- snapshot: createIdleLoginBootstrapSnapshot()
1834
- };
1835
- let lastAuthenticatedAuthState = initialAuthState;
1836
- let didEmitStart = false;
1837
- let didScheduleStop = false;
1838
- let presenceStartPromise = null;
1839
- let presenceStopPromise = null;
1840
- let lastInteractiveDirectoryPath = null;
1841
- const resolveActionAuthState = async () => {
1842
- const storedAuthState = await resolveStoredAuthState(input.worktree, config);
1843
- if (storedAuthState) return storedAuthState;
1844
- return lastAuthenticatedAuthState;
1845
- };
1846
- const emitActionEventForCurrentSession = async ({
1847
- event,
1848
- authState,
1849
- directoryPath
1850
- }) => {
1851
- await emitPluginActionEvent({
1852
- config,
1853
- authState: authState ?? (await resolveActionAuthState()),
1854
- event,
1855
- workspacePath,
1856
- directoryPath
1857
- });
1858
- };
1859
- const schedulePresenceStart = authState => {
1860
- lastAuthenticatedAuthState = authState;
1861
- if (didEmitStart) {
1862
- return presenceStartPromise ?? Promise.resolve();
1863
- }
1864
- didEmitStart = true;
1865
- presenceStartPromise = Promise.all([emitPresenceEvent({
1866
- config,
1867
- authState,
1868
- event: 'START',
1869
- workspacePath
1870
- }), emitActionEventForCurrentSession({
1871
- event: 'START',
1872
- authState,
1873
- directoryPath: lastInteractiveDirectoryPath ?? undefined
1874
- })]).then(() => undefined);
1875
- return presenceStartPromise;
1876
- };
1877
- const schedulePresenceStop = () => {
1878
- if (didScheduleStop) {
1879
- return presenceStopPromise ?? Promise.resolve();
1880
- }
1881
- didScheduleStop = true;
1882
- if (!didEmitStart || !lastAuthenticatedAuthState) {
1883
- presenceStopPromise = Promise.resolve();
1884
- return presenceStopPromise;
1885
- }
1886
- presenceStopPromise = (async () => {
1887
- await presenceStartPromise?.catch(() => undefined);
1888
- await Promise.all([emitPresenceEvent({
1889
- config,
1890
- authState: lastAuthenticatedAuthState,
1891
- event: 'STOP',
1892
- workspacePath
1893
- }), emitActionEventForCurrentSession({
1894
- event: 'STOP',
1895
- authState: lastAuthenticatedAuthState,
1896
- directoryPath: lastInteractiveDirectoryPath ?? undefined
1897
- })]);
1898
- })();
1899
- return presenceStopPromise;
1900
- };
1901
- const scheduleInteractivePresenceStart = async () => {
1902
- const authState = await resolveStoredAuthState(input.worktree, config);
1903
- if (!authState) return;
1904
- await schedulePresenceStart(authState);
1905
- };
1906
- process.once('beforeExit', () => {
1907
- void schedulePresenceStop();
1908
- });
1909
- for (const shutdownSignal of PRESENCE_SHUTDOWN_SIGNALS) {
1910
- try {
1911
- process.once(shutdownSignal, () => {
1912
- void schedulePresenceStop().finally(() => {
1913
- process.exit(PRESENCE_SIGNAL_EXIT_CODES[shutdownSignal]);
1914
- });
1915
- });
1916
- } catch {
1917
- continue;
1918
- }
1919
- }
1920
- const clearPublishedSkillState = () => {
1921
- cache.clear();
1922
- catalogInflight.clear();
1923
- detailCache.clear();
1924
- detailInflight.clear();
1925
- };
1926
- const persistAuthState = async session => {
1927
- const authState = toAuthState(session);
1928
- await writeAuthState(config.authStatePath, authState);
1929
- clearPublishedSkillState();
1930
- return authState;
1931
- };
1932
- const startLoginCompletion = trigger => {
1933
- if (loginBootstrap.promise) {
1934
- return loginBootstrap.promise;
1935
- }
1936
- const startedAt = new Date().toISOString();
1937
- loginBootstrap.snapshot = {
1938
- status: 'starting',
1939
- trigger,
1940
- startedAt,
1941
- expiresAt: null,
1942
- browserUrl: null,
1943
- browserOpenError: null,
1944
- email: null,
1945
- message: null
1946
- };
1947
- const loginPromise = (async () => {
1948
- const loginSignal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
1949
- let loginStart = null;
1950
- try {
1951
- loginStart = await startLoginFlow(loginSignal);
1952
- const browserOpenError = await openBrowser(loginStart.browserUrl);
1953
- loginBootstrap.snapshot = {
1954
- status: 'pending',
1955
- trigger,
1956
- startedAt,
1957
- expiresAt: loginStart.expiresAt,
1958
- browserUrl: loginStart.browserUrl,
1959
- browserOpenError,
1960
- email: null,
1961
- message: browserOpenError ? `Automatic browser open failed. Open ${loginStart.browserUrl} manually.` : `Browser login started for published skill ${trigger}.`
1962
- };
1963
- const callbackPayload = await loginStart.callbackPromise;
1964
- if (callbackPayload.status === 'error') {
1965
- throw new Error(callbackPayload.message);
1966
- }
1967
- if (callbackPayload.state !== loginStart.expectedState) {
1968
- throw new Error('OAuth callback state did not match the original login request.');
1969
- }
1970
- loginBootstrap.snapshot = {
1971
- status: 'pending',
1972
- trigger,
1973
- startedAt,
1974
- expiresAt: loginStart.expiresAt,
1975
- browserUrl: loginStart.browserUrl,
1976
- browserOpenError,
1977
- email: null,
1978
- message: 'OAuth callback received. Finalizing backend session exchange.'
1979
- };
1980
- const pluginSession = await createPluginSession({
1981
- code: callbackPayload.code,
1982
- codeVerifier: loginStart.codeVerifier,
1983
- redirectUri: OIDC_CALLBACK_URL,
1984
- config,
1985
- signal: loginSignal
1986
- });
1987
- const authState = await persistAuthState(pluginSession);
1988
- await emitActionEventForCurrentSession({
1989
- event: 'LOGIN_SUCCESS',
1990
- authState,
1991
- directoryPath: lastInteractiveDirectoryPath ?? undefined
1992
- });
1993
- loginBootstrap.snapshot = {
1994
- status: 'authenticated',
1995
- trigger,
1996
- startedAt,
1997
- expiresAt: authState.expiresAt,
1998
- browserUrl: loginStart.browserUrl,
1999
- browserOpenError,
2000
- email: authState.email,
2001
- message: `Browser login completed successfully for published skill ${trigger}.`
2002
- };
2003
- return authState;
2004
- } catch (error) {
2005
- await emitActionEventForCurrentSession({
2006
- event: 'LOGIN_FAILED',
2007
- directoryPath: lastInteractiveDirectoryPath ?? undefined
2008
- });
2009
- loginBootstrap.snapshot = {
2010
- status: 'failed',
2011
- trigger,
2012
- startedAt,
2013
- expiresAt: loginBootstrap.snapshot.expiresAt,
2014
- browserUrl: loginBootstrap.snapshot.browserUrl,
2015
- browserOpenError: loginBootstrap.snapshot.browserOpenError,
2016
- email: null,
2017
- message: error instanceof Error ? error.message : 'Browser login failed.'
2018
- };
2019
- throw error;
2020
- } finally {
2021
- await loginStart?.closeCallbackServer().catch(() => undefined);
2022
- loginBootstrap.promise = null;
2023
- }
2024
- })();
2025
- loginBootstrap.promise = loginPromise;
2026
- return loginPromise;
2027
- };
2028
- const loadPublishedSkillCatalog = async ({
2029
- directory,
2030
- useCache,
2031
- signal
2032
- }) => {
2033
- const workspaceResolution = await resolveWorkspace({
2034
- config,
2035
- directory
2036
- });
2037
- const directoryPath = workspaceResolution.directoryPath;
2038
- const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
2039
- const cacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
2040
- const cached = cache.get(cacheKey);
2041
- if (useCache && cached && cached.expiresAt > Date.now()) {
2042
- return {
2043
- directoryPath,
2044
- workspaceResolution,
2045
- fetchResult: {
2046
- ...cached.result,
2047
- source: 'cache'
2048
- }
2049
- };
2050
- }
2051
- const inflight = catalogInflight.get(cacheKey);
2052
- if (inflight) {
2053
- return inflight;
2054
- }
2055
- const requestPromise = (async () => {
2056
- const fetchResult = await fetchPublishedSkillsCatalog(input.worktree, config, workspaceResolution, signal, clearPublishedSkillState);
2057
- await maybePersistWorkspaceSlugFromCatalog({
2058
- config,
2059
- resolution: workspaceResolution,
2060
- fetchResult
2061
- });
2062
- cache.set(cacheKey, {
2063
- result: fetchResult,
2064
- expiresAt: Date.now() + CACHE_TTL_MS
2065
- });
2066
- return {
2067
- directoryPath,
2068
- workspaceResolution,
2069
- fetchResult
2070
- };
2071
- })();
2072
- catalogInflight.set(cacheKey, requestPromise);
2073
- try {
2074
- return await requestPromise;
2075
- } finally {
2076
- catalogInflight.delete(cacheKey);
2077
- }
2078
- };
2079
- const loadPublishedSkillDetail = async ({
2080
- workspaceResolution,
2081
- item,
2082
- signal,
2083
- useCache,
2084
- purpose
2085
- }) => {
2086
- const directoryPath = workspaceResolution.directoryPath;
2087
- const preferenceContext = await resolvePublishedSkillPreferenceCacheContext(config);
2088
- const catalogCacheKey = getCatalogCacheKey(workspaceResolution, preferenceContext);
2089
- const cacheKey = getDetailCacheKey(catalogCacheKey, item.skillVersion.id);
2090
- const inflightKey = getDetailInflightKey(catalogCacheKey, item.skillVersion.id, purpose);
2091
- const cached = detailCache.get(cacheKey);
2092
- if (useCache && cached && cached.expiresAt > Date.now()) {
2093
- return {
2094
- ok: true,
2095
- detail: toPublishedSkillDetail({
2096
- ...item,
2097
- publishedArtifact: cached.artifact
2098
- })
2099
- };
2100
- }
2101
- const inflight = detailInflight.get(inflightKey);
2102
- if (inflight) {
2103
- return inflight;
2104
- }
2105
- const requestPromise = (async () => {
2106
- const detailResult = await fetchPublishedSkillDetail({
2107
- worktree: input.worktree,
2108
- config,
2109
- resolution: workspaceResolution,
2110
- skillVersionId: item.skillVersion.id,
2111
- signal,
2112
- onAuthStateChanged: clearPublishedSkillState,
2113
- purpose
2114
- });
2115
- if (!detailResult.ok) {
2116
- return {
2117
- ok: false,
2118
- status: detailResult.result.status,
2119
- output: JSON.stringify({
2120
- pluginId: PLUGIN_ID,
2121
- runtimeMode: 'tool_fetch_only',
2122
- status: detailResult.result.status,
2123
- requestedDirectoryPath: directoryPath,
2124
- workspaceResolution: toWorkspaceResolutionOutput(workspaceResolution),
2125
- requestedSkillVersionId: item.skillVersion.id,
2126
- message: detailResult.result.message,
2127
- fetchedAt: detailResult.result.fetchedAt,
2128
- source: detailResult.result.source
2129
- }, null, 2),
2130
- metadata: {
2131
- status: detailResult.result.status,
2132
- ...toWorkspaceResolutionMetadata(workspaceResolution),
2133
- source: detailResult.result.source
2134
- }
2135
- };
2136
- }
2137
- detailCache.set(cacheKey, {
2138
- artifact: detailResult.artifact,
2139
- expiresAt: Date.now() + CACHE_TTL_MS
2140
- });
2141
- return {
2142
- ok: true,
2143
- detail: toPublishedSkillDetail({
2144
- ...item,
2145
- publishedArtifact: detailResult.artifact
2146
- })
2147
- };
2148
- })();
2149
- detailInflight.set(inflightKey, requestPromise);
2150
- try {
2151
- return await requestPromise;
2152
- } finally {
2153
- detailInflight.delete(inflightKey);
2154
- }
2155
- };
2156
- const loadSystemNoteDetails = async ({
2157
- publishedSkillsResult,
2158
- signal
2159
- }) => {
2160
- if (!publishedSkillsResult.fetchResult.ok) return [];
2161
- const prioritizedItems = [...publishedSkillsResult.fetchResult.payload.skills].sort((left, right) => {
2162
- const leftSummary = toPublishedSkillSummary(left);
2163
- const rightSummary = toPublishedSkillSummary(right);
2164
- if (leftSummary.contextKind === rightSummary.contextKind) return formatSkillLabel(left).localeCompare(formatSkillLabel(right));
2165
- if (leftSummary.contextKind === 'global') return -1;
2166
- if (rightSummary.contextKind === 'global') return 1;
2167
- return 0;
2168
- });
2169
- const detailResults = await Promise.all(prioritizedItems.slice(0, SYSTEM_NOTE_DETAIL_LIMIT).map(item => loadPublishedSkillDetail({
2170
- workspaceResolution: publishedSkillsResult.workspaceResolution,
2171
- item,
2172
- signal,
2173
- useCache: true,
2174
- purpose: 'SYSTEM_CONTEXT'
2175
- })));
2176
- return detailResults.reduce((details, result) => {
2177
- if (!result.ok) return details;
2178
- details.push(result.detail);
2179
- return details;
2180
- }, []);
2181
- };
2182
- const executePublishedSkillsFetchTool = async ({
2183
- args,
2184
- context
2185
- }) => {
2186
- const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
2187
- const requestedSkills = parseRequestedSkillArgs(args);
2188
- const fetchActionDirectoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
2189
- lastInteractiveDirectoryPath = fetchActionDirectoryPath;
2190
- const emitFetchOutcome = async event => {
2191
- await emitActionEventForCurrentSession({
2192
- event,
2193
- directoryPath: fetchActionDirectoryPath
2194
- });
2195
- };
2196
- let publishedSkillsResult = await loadPublishedSkillCatalog({
2197
- directory: requestedDirectory,
2198
- useCache: !args.refresh,
2199
- signal: context.abort
2200
- });
2201
- if (publishedSkillsResult.fetchResult.ok) {
2202
- await scheduleInteractivePresenceStart();
2203
- }
2204
- if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
2205
- try {
2206
- await startLoginCompletion('fetch').then(async authState => {
2207
- await schedulePresenceStart(authState);
2208
- });
2209
- publishedSkillsResult = await loadPublishedSkillCatalog({
2210
- directory: requestedDirectory,
2211
- useCache: false,
2212
- signal: context.abort
2213
- });
2214
- if (publishedSkillsResult.fetchResult.ok) {
2215
- await scheduleInteractivePresenceStart();
2216
- }
2217
- } catch {
2218
- // Return the original fetch failure with the latest login bootstrap snapshot attached.
2219
- }
2220
- }
2221
- if (!publishedSkillsResult.fetchResult.ok) {
2222
- await emitFetchOutcome('FETCH_FAILED');
2223
- return toFetchFailureOutput({
2224
- worktree: input.worktree,
2225
- config,
2226
- publishedSkillsResult,
2227
- loginBootstrapSnapshot: loginBootstrap.snapshot
2228
- });
2229
- }
2230
- const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
2231
- if (!filteredPublishedSkillsResult.fetchResult.ok) {
2232
- await emitFetchOutcome('FETCH_FAILED');
2233
- return toFetchFailureOutput({
2234
- worktree: input.worktree,
2235
- config,
2236
- publishedSkillsResult: filteredPublishedSkillsResult,
2237
- loginBootstrapSnapshot: loginBootstrap.snapshot
2238
- });
2239
- }
2240
- const selection = selectPublishedSkills(filteredPublishedSkillsResult.fetchResult.payload, requestedSkills);
2241
- const isSingleRequest = requestedSkills.length === 1;
2242
- if (requestedSkills.length === 0) {
2243
- const catalog = toPublishedSkillCatalog(filteredPublishedSkillsResult.fetchResult.payload);
2244
- context.metadata({
2245
- title: `opencode-wizard published skills catalog: ${catalog.publishedSkillCount} active`,
2246
- metadata: {
2247
- ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2248
- status: 'ready',
2249
- publishedSkillCount: catalog.publishedSkillCount.toString(),
2250
- globalAssignmentCount: catalog.assignmentCounts.global.toString(),
2251
- projectAssignmentCount: catalog.assignmentCounts.project.toString(),
2252
- userAssignmentCount: catalog.assignmentCounts.user.toString(),
2253
- ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
2254
- }
2255
- });
2256
- await emitFetchOutcome('FETCH_SUCCESS');
2257
- return {
2258
- output: JSON.stringify({
2259
- ...catalog,
2260
- status: 'ready',
2261
- requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2262
- workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2263
- ignoredPublishedSkills: {
2264
- scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
2265
- count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
2266
- },
2267
- fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2268
- source: filteredPublishedSkillsResult.fetchResult.source,
2269
- cacheTtlMs: CACHE_TTL_MS,
2270
- message: args.refresh ? 'Catalog discovery refreshed from the backend. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.' : 'Catalog discovery only. Cached results expire automatically after 30 seconds, or pass `refresh: true` to force a backend refresh immediately. Provide `skill` for one identifier or prefer `skills` for comma/newline-separated multiple identifiers to fetch markdown bodies/details.'
2271
- }, null, 2),
2272
- metadata: {
2273
- status: 'ready',
2274
- ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2275
- source: filteredPublishedSkillsResult.fetchResult.source,
2276
- publishedSkillCount: catalog.publishedSkillCount.toString(),
2277
- globalAssignmentCount: catalog.assignmentCounts.global.toString(),
2278
- projectAssignmentCount: catalog.assignmentCounts.project.toString(),
2279
- userAssignmentCount: catalog.assignmentCounts.user.toString(),
2280
- ignoredSkillCount: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length.toString()
2281
- }
2282
- };
2283
- }
2284
- if (selection.selectedItems.length === 0 && isSingleRequest) {
2285
- await emitFetchOutcome('FETCH_FAILED');
2286
- return {
2287
- output: JSON.stringify({
2288
- pluginId: PLUGIN_ID,
2289
- runtimeMode: 'tool_fetch_only',
2290
- status: 'not_found',
2291
- requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2292
- workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2293
- requestedSkill: requestedSkills[0],
2294
- availableSkills: filteredPublishedSkillsResult.fetchResult.payload.skills.map(toPublishedSkillSummary),
2295
- ignoredPublishedSkills: {
2296
- scopeKey: filteredPublishedSkillsResult.ignoreState.scopeKey,
2297
- count: filteredPublishedSkillsResult.ignoreState.ignoredSkillSlugs.length
2298
- }
2299
- }, null, 2),
2300
- metadata: {
2301
- status: 'not_found',
2302
- ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution)
2303
- }
2304
- };
2305
- }
2306
- let skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
2307
- workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
2308
- item,
2309
- signal: context.abort,
2310
- useCache: !args.refresh,
2311
- purpose: 'TOOL_FETCH'
2312
- })));
2313
- if (skillDetailResults.some(result => !result.ok && result.status === 'missing_auth')) {
2314
- try {
2315
- await startLoginCompletion('fetch').then(async authState => {
2316
- await schedulePresenceStart(authState);
2317
- });
2318
- skillDetailResults = await Promise.all(selection.selectedItems.map(item => loadPublishedSkillDetail({
2319
- workspaceResolution: filteredPublishedSkillsResult.workspaceResolution,
2320
- item,
2321
- signal: context.abort,
2322
- useCache: false,
2323
- purpose: 'TOOL_FETCH'
2324
- })));
2325
- } catch {
2326
- // Return the original detail failure after the login bootstrap attempt updates snapshot state.
2327
- }
2328
- }
2329
- const failedSkillDetail = skillDetailResults.find(result => !result.ok);
2330
- if (failedSkillDetail && !failedSkillDetail.ok) {
2331
- await emitFetchOutcome('FETCH_FAILED');
2332
- return failedSkillDetail;
2333
- }
2334
- const skillDetails = skillDetailResults.map(result => {
2335
- if (!result.ok) {
2336
- throw new Error('Published skill detail result unexpectedly missing after success guard.');
2337
- }
2338
- return result.detail;
2339
- });
2340
- if (isSingleRequest && skillDetails[0]) {
2341
- const detail = skillDetails[0];
2342
- context.metadata({
2343
- title: `opencode-wizard published skill: ${detail.artifactName || detail.skillName}`,
2344
- metadata: {
2345
- ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2346
- skillSlug: detail.skillSlug,
2347
- version: detail.version
2348
- }
2349
- });
2350
- await emitFetchOutcome('FETCH_SUCCESS');
2351
- return {
2352
- output: JSON.stringify({
2353
- pluginId: PLUGIN_ID,
2354
- runtimeMode: 'tool_fetch_only',
2355
- requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2356
- workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2357
- workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
2358
- fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2359
- source: filteredPublishedSkillsResult.fetchResult.source,
2360
- skill: detail
2361
- }, null, 2),
2362
- metadata: {
2363
- status: 'ready',
2364
- ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2365
- source: filteredPublishedSkillsResult.fetchResult.source,
2366
- skillSlug: detail.skillSlug
2367
- }
2368
- };
2369
- }
2370
- context.metadata({
2371
- title: `opencode-wizard published skills fetch: ${skillDetails.length}`,
2372
- metadata: {
2373
- ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2374
- requestedCount: requestedSkills.length.toString(),
2375
- matchedCount: skillDetails.length.toString()
2376
- }
2377
- });
2378
- await emitFetchOutcome('FETCH_SUCCESS');
2379
- return {
2380
- output: JSON.stringify({
2381
- pluginId: PLUGIN_ID,
2382
- runtimeMode: 'tool_fetch_only',
2383
- requestedDirectoryPath: filteredPublishedSkillsResult.directoryPath,
2384
- workspaceResolution: toWorkspaceResolutionOutput(filteredPublishedSkillsResult.workspaceResolution),
2385
- workspace: filteredPublishedSkillsResult.fetchResult.payload.workspace,
2386
- fetchedAt: filteredPublishedSkillsResult.fetchResult.fetchedAt,
2387
- source: filteredPublishedSkillsResult.fetchResult.source,
2388
- requestedSkills,
2389
- missingSkills: selection.missingIdentifiers,
2390
- skills: skillDetails
2391
- }, null, 2),
2392
- metadata: {
2393
- status: selection.missingIdentifiers.length > 0 ? 'partial' : 'ready',
2394
- ...toWorkspaceResolutionMetadata(filteredPublishedSkillsResult.workspaceResolution),
2395
- source: filteredPublishedSkillsResult.fetchResult.source,
2396
- matchedCount: skillDetails.length.toString()
2397
- }
2398
- };
2399
- };
2400
- const executeStatusTool = async ({
2401
- args,
2402
- context
2403
- }) => {
2404
- const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
2405
- let snapshot = await resolvePluginStatusSnapshot({
2406
- worktree: input.worktree,
2407
- directory: requestedDirectory,
2408
- signal: context.abort
2409
- });
2410
- if (snapshot.status === 'missing_auth') {
2411
- try {
2412
- await startLoginCompletion('status').then(async authState => {
2413
- await schedulePresenceStart(authState);
2414
- });
2415
- snapshot = await resolvePluginStatusSnapshot({
2416
- worktree: input.worktree,
2417
- directory: requestedDirectory,
2418
- signal: context.abort
2419
- });
2420
- } catch {
2421
- // Keep returning the safe missing-auth snapshot when interactive login is cancelled or fails.
2422
- }
2423
- }
2424
- if (snapshot.status === 'ready') {
2425
- await scheduleInteractivePresenceStart();
2426
- }
2427
- const metadata = toPluginStatusMetadata(snapshot);
2428
- context.metadata({
2429
- title: `opencode-wizard status: ${snapshot.status} / auth ${snapshot.authState.status}`,
2430
- metadata
2431
- });
2432
- return {
2433
- output: JSON.stringify(toAiFacingPluginStatusSnapshot(snapshot), null, 2),
2434
- metadata
2435
- };
2436
- };
2437
- const executePublishedSkillPreferenceTool = async ({
2438
- args,
2439
- context
2440
- }) => {
2441
- const requestedDirectory = normalizeDirectoryArg(context.directory, args.directory);
2442
- const directoryPath = normalizeRepositoryPath(workspacePath, requestedDirectory);
2443
- lastInteractiveDirectoryPath = directoryPath;
2444
- const requestedSkill = typeof args.skill === 'string' ? args.skill.trim() : '';
2445
- const emitPreferenceOutcome = async event => {
2446
- await emitActionEventForCurrentSession({
2447
- event,
2448
- directoryPath
2449
- });
2450
- };
2451
- try {
2452
- if (!requestedSkill) {
2453
- throw new Error('Published skill preference tool requires a non-empty skill slug, artifact name, or skill name.');
2454
- }
2455
- if (typeof args.action !== 'string') {
2456
- throw new Error('Published skill preference tool requires an action: install, uninstall, ignore, or unignore.');
2457
- }
2458
- const action = toPublishedSkillPreferenceAction(args.action);
2459
- const catalogResult = await loadPublishedSkillCatalog({
2460
- directory: requestedDirectory,
2461
- useCache: true,
2462
- signal: context.abort
2463
- });
2464
- if (!catalogResult.fetchResult.ok) {
2465
- throw new Error(`Cannot resolve published skill preference target: ${catalogResult.fetchResult.message}`);
2466
- }
2467
- const selectableCatalogSkills = catalogResult.fetchResult.payload.catalogSkills.map(item => ({
2468
- ...item,
2469
- assignmentSource: 'CATALOG',
2470
- assignmentType: 'PATH',
2471
- scopePath: '',
2472
- includeChildren: true
2473
- }));
2474
- const preferenceSelection = selectPublishedSkills({
2475
- ...catalogResult.fetchResult.payload,
2476
- skills: [...catalogResult.fetchResult.payload.skills, ...selectableCatalogSkills, ...catalogResult.fetchResult.payload.userPreferences.ignoredSkills]
2477
- }, [requestedSkill]);
2478
- const matchedSkill = preferenceSelection.selectedItems[0];
2479
- if (!matchedSkill) {
2480
- throw new Error(`Published skill preference target was not found for identifier: ${requestedSkill}.`);
2481
- }
2482
- const skillSlug = matchedSkill.skill.slug;
2483
- const preferenceState = action === 'ignore' || action === 'unignore' ? await setPublishedSkillIgnored({
2484
- worktree: input.worktree,
2485
- directory: requestedDirectory,
2486
- skillSlug,
2487
- ignored: action === 'ignore',
2488
- preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project')
2489
- }) : await setPublishedSkillInstalled({
2490
- worktree: input.worktree,
2491
- directory: requestedDirectory,
2492
- skillSlug,
2493
- installed: action === 'install',
2494
- preferenceScope: toPublishedSkillPreferenceScope(args.preferenceScope, 'project')
2495
- });
2496
- await scheduleInteractivePresenceStart();
2497
- await emitPreferenceOutcome('PREFERENCE_SUCCESS');
2498
- const metadata = {
2499
- status: 'updated',
2500
- skillSlug,
2501
- action,
2502
- directoryPath,
2503
- ignoredSkillCount: preferenceState.ignoredSkillSlugs.length.toString()
2504
- };
2505
- context.metadata({
2506
- title: `opencode-wizard published skill preference: ${action} ${skillSlug}`,
2507
- metadata
2508
- });
2509
- return {
2510
- output: JSON.stringify({
2511
- pluginId: PLUGIN_ID,
2512
- status: 'updated',
2513
- requestedIdentifier: requestedSkill,
2514
- skillSlug,
2515
- action,
2516
- requestedDirectoryPath: directoryPath,
2517
- preferenceState,
2518
- message: 'Published skill preference updated through the shared server-backed API; TUI views will reflect this after refresh.'
2519
- }, null, 2),
2520
- metadata
2521
- };
2522
- } catch (error) {
2523
- await emitPreferenceOutcome('PREFERENCE_FAILED');
2524
- throw error;
2525
- }
2526
- };
2527
- return {
2528
- tool: {
2529
- opencode_wizard_published_skills_fetch: tool({
2530
- description: 'Fetch one or multiple wizard-published skill bodies/details for the current scope. Use this for wizard-listed/private/scoped/backend-published skill slugs instead of the native OpenCode skill tool, and after native errors like `Skill "..." not found`; prefer `skills` for multiple identifiers and call with no args to discover the catalog and bootstrap auth when needed',
2531
- args: {
2532
- skill: tool.schema.string().optional().describe('Single skill slug, artifact name, or skill name; backward-compatible with comma/newline-delimited lists'),
2533
- skills: tool.schema.string().optional().describe('One or more comma-separated or newline-separated skill slugs, artifact names, or skill names'),
2534
- directory: tool.schema.string().optional().describe('Optional absolute or relative directory override'),
2535
- refresh: tool.schema.boolean().optional().describe('Bypass the local plugin cache for this request')
2536
- },
2537
- async execute(args, context) {
2538
- return executePublishedSkillsFetchTool({
2539
- args,
2540
- context
2541
- });
2542
- }
2543
- }),
2544
- opencode_wizard_published_skill_preference_set: tool({
2545
- description: 'Install, uninstall, ignore, or unignore a backend-published wizard skill for non-TUI workflows using the same shared server-backed preference API as the TUI overlay',
2546
- args: {
2547
- skill: tool.schema.string().describe('Published skill slug, artifact name, or skill name to update'),
2548
- action: tool.schema.string().describe('Preference action: install, uninstall, ignore, or unignore'),
2549
- preferenceScope: tool.schema.string().optional().describe('Preference scope for the action: project or global; defaults to project'),
2550
- directory: tool.schema.string().optional().describe('Optional absolute or relative directory override')
2551
- },
2552
- async execute(args, context) {
2553
- return executePublishedSkillPreferenceTool({
2554
- args,
2555
- context
2556
- });
2557
- }
2558
- }),
2559
- opencode_wizard_status: tool({
2560
- description: 'Report opencode-wizard plugin status, bootstrap auth when missing, and return a safe auth summary without exposing tokens',
2561
- args: {
2562
- directory: tool.schema.string().optional().describe('Optional absolute or relative directory override')
2563
- },
2564
- async execute(args, context) {
2565
- return executeStatusTool({
2566
- args,
2567
- context
2568
- });
2569
- }
2570
- })
2571
- },
2572
- 'experimental.chat.system.transform': async (_hookInput, output) => {
2573
- let publishedSkillsResult = await loadPublishedSkillCatalog({
2574
- directory: input.directory,
2575
- useCache: true,
2576
- signal: AbortSignal.timeout(5_000)
2577
- });
2578
- if (!publishedSkillsResult.fetchResult.ok && publishedSkillsResult.fetchResult.status === 'missing_auth') {
2579
- try {
2580
- await startLoginCompletion('status').then(async authState => {
2581
- await schedulePresenceStart(authState);
2582
- });
2583
- publishedSkillsResult = await loadPublishedSkillCatalog({
2584
- directory: input.directory,
2585
- useCache: false,
2586
- signal: AbortSignal.timeout(5_000)
2587
- });
2588
- } catch {
2589
- const loginMessage = loginBootstrap.snapshot.message ? ` Last login status: ${loginBootstrap.snapshot.message}` : '';
2590
- 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.`);
2591
- return;
2592
- }
2593
- if (!publishedSkillsResult.fetchResult.ok) {
2594
- output.system.push(`opencode-wizard plugin startup login completed, but published skills are still unavailable: ${publishedSkillsResult.fetchResult.message} No tokens are exposed.`);
2595
- return;
2596
- }
2597
- }
2598
- if (publishedSkillsResult.fetchResult.ok) {
2599
- await scheduleInteractivePresenceStart();
2600
- }
2601
- const filteredPublishedSkillsResult = await filterIgnoredPublishedSkills(config, publishedSkillsResult);
2602
- const details = await loadSystemNoteDetails({
2603
- publishedSkillsResult: filteredPublishedSkillsResult,
2604
- signal: AbortSignal.timeout(5_000)
2605
- });
2606
- const systemNote = buildSystemNote(filteredPublishedSkillsResult, config, details);
2607
- if (!systemNote) return;
2608
- output.system.push(systemNote);
2609
- }
2610
- };
2611
- };
1
+ export { AVAILABLE_PUBLISHED_SKILL_TOOLS, resolveAvailableTools } from './plugin-tools.js';
2
+ export { buildSkillMarkdown, parseRequestedSkillArgs, selectPublishedSkills, toPublishedSkillDetail } from './published-skills-transform.js';
3
+ export { PLUGIN_ID, NATIVE_SKILLS_URL_COMPATIBILITY, buildSystemNote, resolveConfig, resolvePluginStatusSnapshot, resolvePluginStatusSnapshotWithAuthBootstrap, setPublishedSkillIgnored, setPublishedSkillInstalled, toPluginAuthStateSummary, toPublishedSkillCatalog } from './server/runtime.js';
4
+ import { PLUGIN_ID, OpencodeWizardSkillsPlugin } from './server/runtime.js';
2612
5
  export default {
2613
6
  id: PLUGIN_ID,
2614
7
  server: OpencodeWizardSkillsPlugin